From b4b975174df56df982cc4ff8c492a96cf577e93b Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sat, 1 Mar 2025 00:14:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=BE=88=E5=A4=9A?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/extensions.json | 2 +- .vscode/settings.json | 9 +- electron.vite.config.ts | 15 + package.json | 6 + packages/locales/index.ts | 35 ++ packages/locales/languages/en.json | 77 ++++ packages/locales/languages/zh.json | 76 ++++ packages/locales/main.ts | 39 ++ packages/locales/package.json | 12 + pnpm-lock.yaml | 284 ++++++++++++ src/main/commands/BasicCommand.ts | 11 + src/main/modules/zephyr/index.ts | 479 +++++++++++++++++++-- src/renderer/auto-imports.d.ts | 2 + src/renderer/components.d.ts | 1 + src/renderer/src/components/CodeEditor/120x120.png | Bin 0 -> 13456 bytes .../CodeEditor/PlaceholderContentWidget.ts | 57 +++ src/renderer/src/components/CodeEditor/a.d.ts | 1 + .../src/components/CodeEditor/code-editor.vue | 296 +++++++++++++ src/renderer/src/components/CodeEditor/monaco.ts | 16 + src/renderer/src/components/CodeEditor/readme.md | 3 + src/renderer/src/components/CodeEditor/utils.ts | 32 ++ src/renderer/src/components/NavBar.vue | 8 +- src/renderer/src/i18n/index.ts | 26 ++ src/renderer/src/layouts/default.vue | 6 + src/renderer/src/main.ts | 2 + src/renderer/src/pages/browser.vue | 143 ++++++ src/renderer/src/pages/index copy.vue | 149 ------- src/renderer/src/pages/index.vue | 20 +- src/renderer/typed-router.d.ts | 2 +- tsconfig.node.json | 5 +- 30 files changed, 1630 insertions(+), 184 deletions(-) create mode 100644 packages/locales/index.ts create mode 100644 packages/locales/languages/en.json create mode 100644 packages/locales/languages/zh.json create mode 100644 packages/locales/main.ts create mode 100644 packages/locales/package.json create mode 100644 src/renderer/src/components/CodeEditor/120x120.png create mode 100644 src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts create mode 100644 src/renderer/src/components/CodeEditor/a.d.ts create mode 100644 src/renderer/src/components/CodeEditor/code-editor.vue create mode 100644 src/renderer/src/components/CodeEditor/monaco.ts create mode 100644 src/renderer/src/components/CodeEditor/readme.md create mode 100644 src/renderer/src/components/CodeEditor/utils.ts create mode 100644 src/renderer/src/i18n/index.ts create mode 100644 src/renderer/src/pages/browser.vue delete mode 100644 src/renderer/src/pages/index copy.vue diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 8ccf0e7..82d8334 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["dbaeumer.vscode-eslint"] + "recommendations": ["dbaeumer.vscode-eslint", "lokalise.i18n-ally"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 0d09930..4c61b65 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,12 @@ }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "i18n-ally.localesPaths": ["packages/locales/languages"], + "i18n-ally.sourceLanguage": "zh", + "i18n-ally.displayLanguage": "zh", + "i18n-ally.keystyle": "nested", + "i18n-ally.extract.autoDetect": true, + "i18n-ally.enabledFrameworks": ["vue"], + "i18n-ally.enabledParsers": ["json"] } diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 569abc9..30b88af 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -9,6 +9,8 @@ import VueMacros from "unplugin-vue-macros/vite" import { VueRouterAutoImports } from "unplugin-vue-router" import VueRouter from "unplugin-vue-router/vite" import Layouts from "vite-plugin-vue-layouts" +import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite" +import monacoEditorPlugin from "vite-plugin-monaco-editor" export default defineConfig({ main: { @@ -62,6 +64,10 @@ export default defineConfig({ }), }, }), + VueI18nPlugin({ + compositionOnly: false, + include: resolve(__dirname, "packages/locales/languages/**"), + }), Layouts({ layoutsDirs: "src/layouts", pagesDirs: "src/pages", @@ -79,6 +85,7 @@ export default defineConfig({ // add any other imports you were relying on "vue-router/auto": ["useLink"], }, + "vue-i18n", ], dts: true, dirs: ["src/composables"], @@ -89,6 +96,14 @@ export default defineConfig({ dts: true, dirs: ["src/components"], }), + // https://wf0.github.io/example/plugins/Formatter.html + // @ts-ignore ... + monacoEditorPlugin.default({ + publicPath: "monacoeditorwork", + customDistPath() { + return resolve(__dirname, "out/renderer/monacoeditorwork") + }, + }), ], }, }) diff --git a/package.json b/package.json index 2d55432..4153ea0 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@electron-toolkit/eslint-config": "^1.0.2", "@electron-toolkit/eslint-config-ts": "^2.0.0", "@electron-toolkit/tsconfig": "^1.0.1", + "@intlify/unplugin-vue-i18n": "^6.0.3", "@rushstack/eslint-patch": "^1.10.5", "@types/node": "^20.17.19", "@unocss/preset-rem-to-px": "^0.64.1", @@ -58,14 +59,19 @@ "electron-vite": "^2.3.0", "eslint": "^8.57.1", "eslint-plugin-vue": "^9.32.0", + "locales": "workspace:*", + "lodash-es": "^4.17.21", + "monaco-editor": "^0.52.2", "prettier": "^3.5.1", "rotating-file-stream": "^3.2.6", "simplebar-vue": "^2.4.0", "typescript": "^5.7.3", "unocss": "^0.64.1", "vite": "^5.4.14", + "vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-vue-layouts": "^0.11.0", "vue": "^3.5.13", + "vue-i18n": "^11.1.1", "vue-tsc": "^2.1.10" } } diff --git a/packages/locales/index.ts b/packages/locales/index.ts new file mode 100644 index 0000000..e5ad212 --- /dev/null +++ b/packages/locales/index.ts @@ -0,0 +1,35 @@ +const datetimeFormats = { + en: { + short: { + year: "numeric", + month: "short", + day: "numeric", + }, + long: { + year: "numeric", + month: "short", + day: "numeric", + weekday: "short", + hour: "numeric", + minute: "numeric", + }, + }, + zh: { + short: { + year: "numeric", + month: "short", + day: "numeric", + }, + long: { + year: "numeric", + month: "short", + day: "numeric", + weekday: "short", + hour: "numeric", + minute: "numeric", + hour12: true, + }, + }, +} + +export { datetimeFormats } diff --git a/packages/locales/languages/en.json b/packages/locales/languages/en.json new file mode 100644 index 0000000..bc3070e --- /dev/null +++ b/packages/locales/languages/en.json @@ -0,0 +1,77 @@ +{ + "title": "exeaasdaa33 {name} aaaa", + "update": { + "status": { + "IDLE": "check update", + "InitCheckingUpdate": "init checking update", + "CheckingUpdate": "start checking update", + "Error": "checking update error", + "Avaliable": "checked new update v{version}", + "Notavaliable": "current version is newest. v{version}", + "Downloading": "current download progress {percent}%", + "Downloaded": "newest version download, click to install " + } + }, + "setting": { + "tips": { + "notSave": "not save" + }, + "tabs": { + "common": "common", + "editor": "editor", + "update": "update" + }, + "log_path_btn": "open log path", + "update": { + "author": { + "title": "author", + "desc": "who's author", + "placeholder": "please input author's name" + }, + "repo": { + "title": "repository", + "desc": "Updated repository name", + "placeholder": "please input repository's name" + }, + "version": { + "title": "Check Version", + "desc": "Current Version:", + "button": "Check Update" + } + }, + "editor": { + "bg": { + "title": "background", + "desc": "change editor background", + "placeholder": "please input picture link" + }, + "font": { + "title": "font", + "desc": "change editor font", + "placeholder": "please input font name" + } + }, + "language": { + "title": "Language", + "desc": "Switch Language", + "options": { + "zh": "Chinese", + "en": "English" + } + }, + "storagePath": { + "title": "Data Storage Path", + "desc": "Local Data Storage Path", + "buttons": { + "select": "Select Path", + "open": "Open Path" + } + } + }, + "app-menu": { + "about": "about" + }, + "qie-huan-kai-fa-zhe-gong-ju": "ToggleDevtool", + "qu-xiao-quan-ping": "Canel FullScreen", + "quan-ping": "FullScreen" +} diff --git a/packages/locales/languages/zh.json b/packages/locales/languages/zh.json new file mode 100644 index 0000000..c6099ce --- /dev/null +++ b/packages/locales/languages/zh.json @@ -0,0 +1,76 @@ +{ + "title": "aaaaaa2 {name} bbbb", + "update": { + "status": { + "IDLE": "检查更新", + "InitCheckingUpdate": "初始化检查更新", + "CheckingUpdate": "开始检查更新", + "Error": "检查更新出错", + "Avaliable": "检查到新版本 v{version}", + "Notavaliable": "当前版本已经是最新 v{version}", + "Downloading": "当前下载进度{percent}%", + "Downloaded": "新版本下载完毕,点击安装" + } + }, + "setting": { + "tips": { + "notSave": "未保存" + }, + "tabs": { + "common": "通用", + "editor": "编辑器", + "update": "更新" + }, + "log_path_btn": "打开日志目录", + "update": { + "author": { + "title": "作者", + "desc": "更新的仓库作者", + "placeholder": "请输入作者" + }, + "repo": { + "title": "仓库", + "desc": "更新的仓库", + "placeholder": "请输入仓库" + }, + "version": { + "title": "检查更新", + "desc": "当前版本:" + } + }, + "editor": { + "bg": { + "title": "背景", + "desc": "改变编辑器背景", + "placeholder": "请输入图片链接" + }, + "font": { + "title": "字体", + "desc": "改变编辑器字体", + "placeholder": "请输入字体" + } + }, + "language": { + "title": "语言", + "desc": "切换语言显示", + "options": { + "zh": "中文", + "en": "英文" + } + }, + "storagePath": { + "title": "数据保存路径", + "desc": "本地数据保存地址", + "buttons": { + "select": "选择目录", + "open": "打开目录" + } + } + }, + "app-menu": { + "about": "关于" + }, + "qie-huan-kai-fa-zhe-gong-ju": "切换开发者工具", + "qu-xiao-quan-ping": "取消全屏", + "quan-ping": "全屏" +} diff --git a/packages/locales/main.ts b/packages/locales/main.ts new file mode 100644 index 0000000..010cd8b --- /dev/null +++ b/packages/locales/main.ts @@ -0,0 +1,39 @@ +import { app } from "electron" +import { get } from "lodash-es" + +import zh from "./languages/zh.json" +import en from "./languages/en.json" + +type FlattenObject = T extends object + ? { + [K in keyof T & (string | number)]: FlattenObject + }[keyof T & (string | number)] + : Prefix + +type FlattenKeys = FlattenObject + +type TranslationKey = FlattenKeys + +class Locale { + locale: string = "en" + + constructor() { + try { + this.locale = app.getLocale() + } catch (e) { + console.log(e) + } + } + + isCN(): boolean { + return this.locale.startsWith("zh") + } + + t(key: TranslationKey): string { + return this.isCN() ? get(zh, key) : get(en, key) + } +} + +const Locales = new Locale() +export default Locales +export { Locales } diff --git a/packages/locales/package.json b/packages/locales/package.json new file mode 100644 index 0000000..45d79a8 --- /dev/null +++ b/packages/locales/package.json @@ -0,0 +1,12 @@ +{ + "name": "locales", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2919a8..482c71c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: '@electron-toolkit/tsconfig': specifier: ^1.0.1 version: 1.0.1(@types/node@20.17.19) + '@intlify/unplugin-vue-i18n': + specifier: ^6.0.3 + version: 6.0.3(@vue/compiler-dom@3.5.13)(eslint@8.57.1)(rollup@4.26.0)(typescript@5.7.3)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)) '@rushstack/eslint-patch': specifier: ^1.10.5 version: 1.10.5 @@ -102,6 +105,15 @@ importers: eslint-plugin-vue: specifier: ^9.32.0 version: 9.32.0(eslint@8.57.1) + locales: + specifier: workspace:* + version: link:packages/locales + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + monaco-editor: + specifier: ^0.52.2 + version: 0.52.2 prettier: specifier: ^3.5.1 version: 3.5.1 @@ -120,16 +132,24 @@ importers: vite: specifier: ^5.4.14 version: 5.4.14(@types/node@20.17.19)(sass@1.85.0) + vite-plugin-monaco-editor: + specifier: ^1.1.0 + version: 1.1.0(monaco-editor@0.52.2) vite-plugin-vue-layouts: specifier: ^0.11.0 version: 0.11.0(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)) vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.7.3) + vue-i18n: + specifier: ^11.1.1 + version: 11.1.1(vue@3.5.13(typescript@5.7.3)) vue-tsc: specifier: ^2.1.10 version: 2.1.10(typescript@5.7.3) + packages/locales: {} + packages: 7zip-bin@5.2.0: @@ -675,6 +695,69 @@ packages: '@iconify/utils@2.1.33': resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==} + '@intlify/bundle-utils@10.0.0': + resolution: {integrity: sha512-BR5yLOkF2dzrARTbAg7RGAIPcx9Aark7p1K/0O285F7rfzso9j2dsa+S4dA67clZ0rToZ10NSSTfbyUptVu7Bg==} + engines: {node: '>= 18'} + peerDependencies: + petite-vue-i18n: '*' + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/core-base@11.1.1': + resolution: {integrity: sha512-bb8gZvoeKExCI2r/NVCK9E4YyOkvYGaSCPxVZe8T0jz8aX+dHEOZWxK06Z/Y9mWRkJfBiCH4aOhDF1yr1t5J8Q==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@11.0.0-rc.1': + resolution: {integrity: sha512-TGw2uBfuTFTegZf/BHtUQBEKxl7Q/dVGLoqRIdw8lFsp9g/53sYn5iD+0HxIzdYjbWL6BTJMXCPUHp9PxDTRPw==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@11.1.1': + resolution: {integrity: sha512-4iEsUZ3aF7jXY19CJFN5VP+pPyLITD9FVsjB13z9TU1UxaZLlFsmNhvRxlPDSOfHAP5RpNF2QKKdZ3DHVf4Yzw==} + engines: {node: '>= 16'} + + '@intlify/shared@11.0.0-rc.1': + resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==} + engines: {node: '>= 16'} + + '@intlify/shared@11.1.1': + resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==} + engines: {node: '>= 16'} + + '@intlify/unplugin-vue-i18n@6.0.3': + resolution: {integrity: sha512-9ZDjBlhUHtgjRl23TVcgfJttgu8cNepwVhWvOv3mUMRDAhjW0pur1mWKEUKr1I8PNwE4Gvv2IQ1xcl4RL0nG0g==} + engines: {node: '>= 18'} + peerDependencies: + petite-vue-i18n: '*' + vue: ^3.2.25 + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/vue-i18n-extensions@8.0.0': + resolution: {integrity: sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==} + engines: {node: '>= 18'} + peerDependencies: + '@intlify/shared': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@vue/compiler-dom': ^3.0.0 + vue: ^3.0.0 + vue-i18n: ^9.0.0 || ^10.0.0 || ^11.0.0 + peerDependenciesMeta: + '@intlify/shared': + optional: true + '@vue/compiler-dom': + optional: true + vue: + optional: true + vue-i18n: + optional: true + '@inversifyjs/common@1.4.0': resolution: {integrity: sha512-qfRJ/3iOlCL/VfJq8+4o5X4oA14cZSBbpAmHsYj8EsIit1xDndoOl0xKOyglKtQD4u4gdNVxMHx4RWARk/I4QA==} @@ -1088,6 +1171,10 @@ packages: resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.25.0': + resolution: {integrity: sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1102,6 +1189,10 @@ packages: resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.25.0': + resolution: {integrity: sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1111,6 +1202,12 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@8.25.0': + resolution: {integrity: sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + '@typescript-eslint/utils@7.18.0': resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} engines: {node: ^18.18.0 || >=20.0.0} @@ -1121,6 +1218,10 @@ packages: resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@8.25.0': + resolution: {integrity: sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -1976,6 +2077,11 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + eslint-config-prettier@9.1.0: resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true @@ -2010,6 +2116,10 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2020,6 +2130,11 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -2410,6 +2525,10 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-eslint-parser@2.4.0: + resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -2449,6 +2568,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -2585,6 +2707,9 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + monaco-editor@0.52.2: + resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} engines: {node: '>=10'} @@ -3039,6 +3164,12 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-api-utils@2.0.1: + resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-macro@0.1.17: resolution: {integrity: sha512-VAep+VT2oDb5KOrmaHvuRWOnkwJU0BR1XAqulCVPF3zO6VkmrH1xc1nS5SrNT4uQJVA3f35QfvCXQwLrCOSRcw==} @@ -3208,6 +3339,11 @@ packages: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} + vite-plugin-monaco-editor@1.1.0: + resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==} + peerDependencies: + monaco-editor: '>=0.33.0' + vite-plugin-vue-layouts@0.11.0: resolution: {integrity: sha512-uh6NW7lt+aOXujK4eHfiNbeo55K9OTuB7fnv+5RVc4OBn/cZull6ThXdYH03JzKanUfgt6QZ37NbbtJ0og59qw==} peerDependencies: @@ -3271,6 +3407,12 @@ packages: peerDependencies: vue: ^3.4.37 + vue-i18n@11.1.1: + resolution: {integrity: sha512-0P6DkKy96R4Wh2sIZJEHw8ivnlD1pnB6Ib/eldoF1SUpQutfKZv6aMqZwICS1gW0rwq24ZSXw7y3jW+PRVYqWA==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + vue-router@4.5.0: resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} peerDependencies: @@ -3331,6 +3473,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml-eslint-parser@1.3.0: + resolution: {integrity: sha512-E/+VitOorXSLiAqtTd7Yqax0/pAS3xaYMP+AUUJGOK1OZG3rhcj9fcJOM5HJ2VrP1FrStVCWr1muTfQCdj4tAA==} + engines: {node: ^14.17.0 || >=16.0.0} + yaml@2.7.0: resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} engines: {node: '>= 14'} @@ -3868,6 +4014,75 @@ snapshots: transitivePeerDependencies: - supports-color + '@intlify/bundle-utils@10.0.0(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))': + dependencies: + '@intlify/message-compiler': 11.0.0-rc.1 + '@intlify/shared': 11.0.0-rc.1 + acorn: 8.14.0 + escodegen: 2.1.0 + estree-walker: 2.0.2 + jsonc-eslint-parser: 2.4.0 + mlly: 1.7.4 + source-map-js: 1.2.1 + yaml-eslint-parser: 1.3.0 + optionalDependencies: + vue-i18n: 11.1.1(vue@3.5.13(typescript@5.7.3)) + + '@intlify/core-base@11.1.1': + dependencies: + '@intlify/message-compiler': 11.1.1 + '@intlify/shared': 11.1.1 + + '@intlify/message-compiler@11.0.0-rc.1': + dependencies: + '@intlify/shared': 11.0.0-rc.1 + source-map-js: 1.2.1 + + '@intlify/message-compiler@11.1.1': + dependencies: + '@intlify/shared': 11.1.1 + source-map-js: 1.2.1 + + '@intlify/shared@11.0.0-rc.1': {} + + '@intlify/shared@11.1.1': {} + + '@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@8.57.1)(rollup@4.26.0)(typescript@5.7.3)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) + '@intlify/bundle-utils': 10.0.0(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3))) + '@intlify/shared': 11.1.1 + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)) + '@rollup/pluginutils': 5.1.4(rollup@4.26.0) + '@typescript-eslint/scope-manager': 8.25.0 + '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.7.3) + debug: 4.4.0 + fast-glob: 3.3.3 + js-yaml: 4.1.0 + json5: 2.2.3 + pathe: 1.1.2 + picocolors: 1.1.1 + source-map-js: 1.2.1 + unplugin: 1.16.1 + vue: 3.5.13(typescript@5.7.3) + optionalDependencies: + vue-i18n: 11.1.1(vue@3.5.13(typescript@5.7.3)) + transitivePeerDependencies: + - '@vue/compiler-dom' + - eslint + - rollup + - supports-color + - typescript + + '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))': + dependencies: + '@babel/parser': 7.26.9 + optionalDependencies: + '@intlify/shared': 11.1.1 + '@vue/compiler-dom': 3.5.13 + vue: 3.5.13(typescript@5.7.3) + vue-i18n: 11.1.1(vue@3.5.13(typescript@5.7.3)) + '@inversifyjs/common@1.4.0': {} '@inversifyjs/core@1.3.5(reflect-metadata@0.2.2)': @@ -4212,6 +4427,11 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/scope-manager@8.25.0': + dependencies: + '@typescript-eslint/types': 8.25.0 + '@typescript-eslint/visitor-keys': 8.25.0 + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.7.3) @@ -4226,6 +4446,8 @@ snapshots: '@typescript-eslint/types@7.18.0': {} + '@typescript-eslint/types@8.25.0': {} + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.7.3)': dependencies: '@typescript-eslint/types': 7.18.0 @@ -4241,6 +4463,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.25.0(typescript@5.7.3)': + dependencies: + '@typescript-eslint/types': 8.25.0 + '@typescript-eslint/visitor-keys': 8.25.0 + debug: 4.4.0 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 2.0.1(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.7.3)': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) @@ -4257,6 +4493,11 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@8.25.0': + dependencies: + '@typescript-eslint/types': 8.25.0 + eslint-visitor-keys: 4.2.0 + '@ungap/structured-clone@1.2.0': {} '@unocss/astro@0.64.1(rollup@4.26.0)(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue@3.5.13(typescript@5.7.3))': @@ -5451,6 +5692,14 @@ snapshots: escape-string-regexp@5.0.0: {} + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + eslint-config-prettier@9.1.0(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -5485,6 +5734,8 @@ snapshots: eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@4.2.0: {} + eslint@8.57.1: dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1) @@ -5534,6 +5785,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.0) eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -5954,6 +6207,13 @@ snapshots: json5@2.2.3: {} + jsonc-eslint-parser@2.4.0: + dependencies: + acorn: 8.14.0 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + semver: 7.6.3 + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -5997,6 +6257,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash.defaults@4.2.0: {} lodash.difference@4.5.0: {} @@ -6114,6 +6376,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.5.4 + monaco-editor@0.52.2: {} + mrmime@2.0.0: {} ms@2.1.3: {} @@ -6586,6 +6850,10 @@ snapshots: dependencies: typescript: 5.7.3 + ts-api-utils@2.0.1(typescript@5.7.3): + dependencies: + typescript: 5.7.3 + ts-macro@0.1.17(rollup@4.26.0)(typescript@5.7.3): dependencies: '@rollup/pluginutils': 5.1.3(rollup@4.26.0) @@ -6836,6 +7104,10 @@ snapshots: extsprintf: 1.4.1 optional: true + vite-plugin-monaco-editor@1.1.0(monaco-editor@0.52.2): + dependencies: + monaco-editor: 0.52.2 + vite-plugin-vue-layouts@0.11.0(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3)): dependencies: debug: 4.4.0 @@ -6879,6 +7151,13 @@ snapshots: dependencies: vue: 3.5.13(typescript@5.7.3) + vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)): + dependencies: + '@intlify/core-base': 11.1.1 + '@intlify/shared': 11.1.1 + '@vue/devtools-api': 6.6.4 + vue: 3.5.13(typescript@5.7.3) + vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)): dependencies: '@vue/devtools-api': 6.6.4 @@ -6933,6 +7212,11 @@ snapshots: yallist@4.0.0: {} + yaml-eslint-parser@1.3.0: + dependencies: + eslint-visitor-keys: 3.4.3 + yaml: 2.7.0 + yaml@2.7.0: {} yargs-parser@21.1.1: {} diff --git a/src/main/commands/BasicCommand.ts b/src/main/commands/BasicCommand.ts index da28eab..053687e 100644 --- a/src/main/commands/BasicCommand.ts +++ b/src/main/commands/BasicCommand.ts @@ -1,14 +1,17 @@ import { app, dialog } from "electron" import { inject } from "inversify" +import Commands from "main/modules/commands" import Tabs from "main/modules/tabs" import WindowManager from "main/modules/window-manager" export default class BasicCommand { constructor( + @inject(Commands) private _Commands: Commands, @inject(WindowManager) private _WindowManager: WindowManager, @inject(Tabs) private _Tabs: Tabs, ) { // + console.log(this._Commands) } toggleDevTools() { @@ -26,6 +29,14 @@ export default class BasicCommand { } } + isFullscreen() { + const focusedWindow = this._WindowManager.getFocusWindow() + if (focusedWindow) { + return focusedWindow!.isFullScreen() + } + return false + } + relunch() { app.relaunch() app.exit() diff --git a/src/main/modules/zephyr/index.ts b/src/main/modules/zephyr/index.ts index 8f787d8..510ce8a 100644 --- a/src/main/modules/zephyr/index.ts +++ b/src/main/modules/zephyr/index.ts @@ -2,47 +2,474 @@ import { session, net } from "electron" import { injectable } from "inversify" import BaseClass from "main/base/base" import _debug from "debug" +import fs from "fs" +import path from "path" +import { app } from "electron" const debug = _debug("app:zephyr") +/** + * Zephyr 模块 - 安全的本地文件访问协议 + * + * 使用说明: + * 1. 访问格式:zephyr://<操作>/<文件路径> + * 操作类型: + * - r/ : 只读访问(read) + * - w/ : 写入访问(write)[未实现] + * - rw/ : 读写访问(read-write)[未实现] + * - t/ : 临时文件访问(temp)[未实现] + * + * 2. 访问示例: + * - 只读文件:zephyr://r/D:/documents/test.txt + * - 应用数据:zephyr://r/app-data/config.json + * - 临时文件:zephyr://t/cache/temp.json + * + * 3. 安全限制: + * - 仅支持以下文件类型:.txt, .json, .md + * - 必须在 ALLOWED_PATHS 白名单中的路径才能访问 + * - 不同操作类型有不同的权限控制 + * + * 4. 配置示例: + * ```typescript + * zephyr.setAllowedPaths({ + * // 只读路径 + * read: [ + * "D:/documents", + * app.getPath("documents") + * ], + * // 临时文件路径 + * temp: [ + * app.getPath("temp") + * ] + * }); + * ``` + */ + +/** + * + * 配置写入权限 +zephyr.setAllowedPaths({ + write: [ + path.join(app.getPath("userData"), "data"), + "D:/allowed-write-path" + ] +}) + +// 写入文件 +fetch("zephyr://w/path/to/file.json", { + method: "POST", + body: JSON.stringify({ data: "test" }) +}) + +// 写入文本 +fetch("zephyr://w/path/to/file.txt", { + method: "POST", + body: "Hello World" +}) + */ + @injectable() class Zephyr extends BaseClass { - constructor( - // @inject(IOC) private _IOC: IOC - ) { + // private readonly ALLOWED_PATHS: string[] = [] // 可以在这里定义允许访问的路径白名单 + private readonly ALLOWED_EXTENSIONS: string[] = [".txt", ".json", ".md"] // 允许的文件类型 + private readonly MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB + // private readonly SAFE_PATH_PATTERN = /^[a-zA-Z0-9\s\-_\/\\:\.]+$/ + + // 定义操作类型 + private readonly OPERATIONS = { + READ: "r", + WRITE: "w", + READWRITE: "rw", + TEMP: "t", + } as const + + private readonly pathConfig: { + read: string[] + temp: string[] + write: string[] + } = { + read: [], + temp: [], + write: [], + } + + // 文件锁定相关 + private readonly fileLocks = new Map() + + // 访问频率限制相关 + private readonly rateLimiter = new Map() + private readonly MAX_REQUESTS = 10 // 每个文件在时间窗口内的最大请求次数 + private readonly WINDOW_MS = 60000 // 时间窗口:1分钟 + + // 审计日志相关 + private readonly LOG_FILE = path.join(app.getPath("logs"), "zephyr-access.log") + + constructor() { super() this.interceptHandlerZephyr = this.interceptHandlerZephyr.bind(this) + this.initLogFile() debug("zephyr init") } + private async initLogFile() { + const logDir = path.dirname(this.LOG_FILE) + await fs.promises.mkdir(logDir, { recursive: true }) + } + + // 文件锁定机制 + private async acquireFileLock(filePath: string): Promise { + if (this.fileLocks.get(filePath)) { + return false + } + this.fileLocks.set(filePath, true) + return true + } + + private releaseFileLock(filePath: string): void { + this.fileLocks.delete(filePath) + } + + // 访问频率限制 + private isRateLimited(filePath: string): boolean { + // const now = Date.now() + const count = this.rateLimiter.get(filePath) || 0 + + if (count >= this.MAX_REQUESTS) { + debug("访问频率超限:", filePath) + return true + } + + this.rateLimiter.set(filePath, count + 1) + setTimeout(() => { + const currentCount = this.rateLimiter.get(filePath) + if (currentCount && currentCount > 0) { + this.rateLimiter.set(filePath, currentCount - 1) + } + }, this.WINDOW_MS) + + return false + } + + // 审计日志 + private async logAccess(operation: string, filePath: string, success: boolean, details?: string) { + const timestamp = new Date().toISOString() + const logEntry = { + timestamp, + operation, + filePath, + success, + details, + } + + try { + await fs.promises.appendFile(this.LOG_FILE, JSON.stringify(logEntry) + "\n", "utf8") + } catch (error) { + debug("写入审计日志失败:", error) + } + } + + // 文件内容验证 + private async validateFileContent(filePath: string): Promise { + try { + const ext = path.extname(filePath).toLowerCase() + const content = await fs.promises.readFile(filePath, "utf8") + + switch (ext) { + case ".json": + JSON.parse(content) + return true + case ".md": + // 可以添加 Markdown 验证逻辑 + return content.length > 0 + case ".txt": + // 文本文件验证 + return content.length > 0 + default: + return false + } + } catch { + return false + } + } + destroy() { - // TODO + const ses = session.defaultSession + ses.protocol.unhandle("zephyr") + this.fileLocks.clear() + this.rateLimiter.clear() + debug("zephyr destroyed") } + init(partition?: string) { const ses = partition ? session.fromPartition(partition) : session.defaultSession ses.protocol.handle("zephyr", this.interceptHandlerZephyr) - console.log(32423) - } - async interceptHandlerZephyr(request: Request) { - if (request.url.startsWith("zephyr://")) { - let curPath = request.url.replace(/^zephyr:\/\//, "") - let isPathRead = false - if (curPath.startsWith("$path/")) { - isPathRead = true - curPath = curPath.replace(/^\$path\//, "") - } - if (isPathRead) { - console.log("安全读取本地目录") - // 检查文件的安全性 - const headers: HeadersInit = {} - headers["content-type"] = "text/txt" - return new Response(curPath, { - status: 200, - headers: Object.keys(headers).length ? headers : undefined, - }) - } - } - return net.fetch(request.url, request) + debug("zephyr initialized with partition:", partition) + } + + setAllowedPaths(config: Partial) { + Object.assign(this.pathConfig, config) + debug("Updated allowed paths:", this.pathConfig) + } + + private isValidPath(filePath: string): boolean { + try { + // 规范化路径 + const normalizedPath = path.normalize(filePath) + + // Windows 路径特殊处理 + const isWindowsPath = /^[a-z]:/i.test(normalizedPath) + + // 检查基本字符(排除特殊字符) + // 允许驱动器冒号,但排除其他特殊字符 + const basicCheck = isWindowsPath ? /^[a-z]:[^<>"|?*]+$/i.test(normalizedPath) : /^[^<>:"|?*]+$/i.test(normalizedPath) + + return basicCheck && (isWindowsPath || normalizedPath.startsWith("/")) + } catch { + return false + } + } + + private async isPathSafe(filePath: string, operation: string): Promise { + try { + // 1. 基本路径检查 + if (!this.isValidPath(filePath)) { + debug("不安全的路径字符:", filePath) + return false + } + + // 2. 检查是否包含 .. 路径 + if (filePath.includes("..")) { + debug("检测到路径遍历尝试") + return false + } + + // 3. 检查符号链接 + if (await this.isSymlink(filePath)) { + debug("不允许访问符号链接") + return false + } + + // 4. 检查文件大小 + if (!(await this.checkFileSize(filePath))) { + debug("文件超出大小限制") + return false + } + + // 5. 文件类型检查 + const ext = path.extname(filePath).toLowerCase() + if (!this.ALLOWED_EXTENSIONS.includes(ext)) { + debug("不允许的文件类型:", ext) + return false + } + + // 6. 权限检查 + const allowedPaths = this.getPathsByOperation(operation) + if (!allowedPaths) return false + + // 7. 确保路径在允许范围内 + const isInAllowedPath = allowedPaths.some(allowedPath => { + const resolvedAllowed = path.resolve(allowedPath) + const resolvedTarget = path.resolve(filePath) + return resolvedTarget.startsWith(resolvedAllowed) + }) + + if (!isInAllowedPath) { + debug("路径不在允许范围内") + return false + } + + // 添加频率限制检查 + if (this.isRateLimited(filePath)) { + await this.logAccess(operation, filePath, false, "访问频率超限") + return false + } + + // 添加文件内容验证 + if (!(await this.validateFileContent(filePath))) { + await this.logAccess(operation, filePath, false, "文件内容验证失败") + return false + } + + await this.logAccess(operation, filePath, true) + return true + } catch (error: any) { + await this.logAccess(operation, filePath, false, error.message) + debug("路径安全检查错误:", error) + return false + } + } + + async interceptHandlerZephyr(request: Request): Promise { + try { + if (!request.url.startsWith("zephyr://")) { + return net.fetch(request.url, request) + } + + const urlParts = request.url.replace(/^zephyr:\/\//, "").split("/") + const operation = urlParts[0] + const filePath = path.normalize(urlParts.slice(1).join("/")) + + if (!operation || !filePath) { + return new Response("Invalid URL format", { status: 400 }) + } + + if (!(await this.isPathSafe(filePath, operation))) { + debug("访问被拒绝:", filePath) + return new Response("Access Denied", { status: 403 }) + } + + // 处理不同的操作类型 + switch (operation) { + case this.OPERATIONS.READ: + return await this.handleReadOperation(filePath) + case this.OPERATIONS.WRITE: + return await this.handleWriteOperation(filePath, request) + default: + return new Response("Operation not supported", { status: 400 }) + } + } catch (error) { + debug("处理请求错误:", error) + return new Response("Internal Server Error", { status: 500 }) + } + } + + private async handleReadOperation(filePath: string): Promise { + const cleanup = async (error?: Error): Promise => { + this.releaseFileLock(filePath) + if (error) { + await this.logAccess("READ", filePath, false, error.message) + return new Response("Internal Server Error", { status: 500 }) + } + return new Response("OK", { status: 200 }) + } + + try { + if (!(await this.acquireFileLock(filePath))) { + await this.logAccess("READ", filePath, false, "文件已锁定") + return new Response("File is locked", { status: 423 }) + } + + const stream = fs.createReadStream(filePath, { + flags: "r", + encoding: "utf8", + }) + + const timeout = setTimeout(() => { + stream.destroy() + cleanup(new Error("读取超时")) + }, 5000) + + const response = new Response(stream as any, { + status: 200, + headers: { + "content-type": this.getContentType(filePath), + "cache-control": "no-cache", + }, + }) + + stream.on("error", error => { + clearTimeout(timeout) + cleanup(error) + }) + + stream.on("end", () => { + clearTimeout(timeout) + cleanup() + }) + + return response + } catch (error) { + return await cleanup(error as Error) + } + } + + private getContentType(filePath: string): string { + const ext = path.extname(filePath).toLowerCase() + const contentTypes: Record = { + ".txt": "text/plain", + ".json": "application/json", + ".md": "text/markdown", + } + return contentTypes[ext] || "application/octet-stream" + } + + private async isSymlink(filePath: string): Promise { + try { + const stats = await fs.promises.lstat(filePath) + return stats.isSymbolicLink() + } catch { + return false + } + } + + private async checkFileSize(filePath: string): Promise { + try { + const stats = await fs.promises.stat(filePath) + return stats.size <= this.MAX_FILE_SIZE + } catch { + return false + } + } + + private getPathsByOperation(operation: string): string[] | null { + switch (operation) { + case this.OPERATIONS.READ: + return this.pathConfig.read + case this.OPERATIONS.TEMP: + return this.pathConfig.temp + case this.OPERATIONS.WRITE: + return this.pathConfig.write + default: + debug("未知的操作类型:", operation) + return null + } + } + + private async handleWriteOperation(filePath: string, request: Request): Promise { + const cleanup = async (error?: Error): Promise => { + this.releaseFileLock(filePath) + if (error) { + await this.logAccess("WRITE", filePath, false, error.message) + return new Response("Write failed: " + error.message, { status: 500 }) + } + return new Response("Write successful", { status: 200 }) + } + + try { + // 1. 获取文件锁 + if (!(await this.acquireFileLock(filePath))) { + return new Response("File is locked", { status: 423 }) + } + + // 2. 确保目标目录存在 + await fs.promises.mkdir(path.dirname(filePath), { recursive: true }) + + // 3. 获取请求内容 + const content = await request.text() + + // 4. 验证内容大小 + if (content.length > this.MAX_FILE_SIZE) { + return cleanup(new Error("Content too large")) + } + + // 5. 验证文件类型和内容 + const ext = path.extname(filePath).toLowerCase() + if (ext === ".json") { + try { + JSON.parse(content) + } catch { + return cleanup(new Error("Invalid JSON content")) + } + } + + // 6. 写入文件 + await fs.promises.writeFile(filePath, content, "utf8") + await this.logAccess("WRITE", filePath, true) + + return cleanup() + } catch (error) { + return cleanup(error as Error) + } } } diff --git a/src/renderer/auto-imports.d.ts b/src/renderer/auto-imports.d.ts index f3961c9..4098275 100644 --- a/src/renderer/auto-imports.d.ts +++ b/src/renderer/auto-imports.d.ts @@ -177,6 +177,7 @@ declare global { const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] const useGamepad: typeof import('@vueuse/core')['useGamepad'] const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] + const useI18n: typeof import('vue-i18n')['useI18n'] const useId: typeof import('vue')['useId'] const useIdle: typeof import('@vueuse/core')['useIdle'] const useImage: typeof import('@vueuse/core')['useImage'] @@ -478,6 +479,7 @@ declare module 'vue' { readonly useFullscreen: UnwrapRef readonly useGamepad: UnwrapRef readonly useGeolocation: UnwrapRef + readonly useI18n: UnwrapRef readonly useId: UnwrapRef readonly useIdle: UnwrapRef readonly useImage: UnwrapRef diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts index 86299cc..4aa1e1e 100644 --- a/src/renderer/components.d.ts +++ b/src/renderer/components.d.ts @@ -9,6 +9,7 @@ export {} declare module 'vue' { export interface GlobalComponents { AdjustLine: typeof import('./src/components/AdjustLine.vue')['default'] + CodeEditor: typeof import('./src/components/CodeEditor/code-editor.vue')['default'] NavBar: typeof import('./src/components/NavBar.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/src/renderer/src/components/CodeEditor/120x120.png b/src/renderer/src/components/CodeEditor/120x120.png new file mode 100644 index 0000000000000000000000000000000000000000..db0a9639399e010e962fb56f260e59118e0a99b9 GIT binary patch literal 13456 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 this.onDidChangeModelContent()) + // ensure that on initial load the placeholder is shown + this.onDidChangeModelContent() + } + + private onDidChangeModelContent(): void { + if (this.editor.getValue() === "") { + this.editor.addContentWidget(this) + } else { + this.editor.removeContentWidget(this) + } + } + + getId(): string { + return PlaceholderContentWidget.ID + } + + getDomNode(): HTMLElement { + if (!this.domNode) { + this.domNode = document.createElement("div") + this.domNode.style.width = "max-content" + this.domNode.style.pointerEvents = "none" + this.domNode.textContent = this.placeholder + this.domNode.style.fontStyle = "italic" + this.editor.applyFontInfo(this.domNode) + } + + return this.domNode + } + + getPosition(): monaco.editor.IContentWidgetPosition | null { + return { + position: { lineNumber: 1, column: 1 }, + preference: [monaco.editor.ContentWidgetPositionPreference.EXACT], + } + } + + dispose(): void { + this.editor.removeContentWidget(this) + } +} diff --git a/src/renderer/src/components/CodeEditor/a.d.ts b/src/renderer/src/components/CodeEditor/a.d.ts new file mode 100644 index 0000000..44df1a0 --- /dev/null +++ b/src/renderer/src/components/CodeEditor/a.d.ts @@ -0,0 +1 @@ +type A = string \ No newline at end of file diff --git a/src/renderer/src/components/CodeEditor/code-editor.vue b/src/renderer/src/components/CodeEditor/code-editor.vue new file mode 100644 index 0000000..7bf97c0 --- /dev/null +++ b/src/renderer/src/components/CodeEditor/code-editor.vue @@ -0,0 +1,296 @@ + + + + + diff --git a/src/renderer/src/components/CodeEditor/monaco.ts b/src/renderer/src/components/CodeEditor/monaco.ts new file mode 100644 index 0000000..a4982ec --- /dev/null +++ b/src/renderer/src/components/CodeEditor/monaco.ts @@ -0,0 +1,16 @@ +// import 'monaco-editor/esm/vs/editor/editor.all.js'; + +// import 'monaco-editor/esm/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.js'; +// import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +// import 'monaco-editor/esm/vs/basic-languages/monaco.contribution.js'; + +// import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution.js'; +// import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js'; +// import 'monaco-editor/esm/vs/basic-languages/html/html.contribution.js'; +// import 'monaco-editor/esm/vs/basic-languages/css/css.contribution.js'; +// import 'monaco-editor/esm/vs/basic-languages/java/java.contribution.js'; + +// 导入全部特性 +import * as monaco from "monaco-editor" + +export { monaco } diff --git a/src/renderer/src/components/CodeEditor/readme.md b/src/renderer/src/components/CodeEditor/readme.md new file mode 100644 index 0000000..b43aab9 --- /dev/null +++ b/src/renderer/src/components/CodeEditor/readme.md @@ -0,0 +1,3 @@ +占位符 +https://github.com/Microsoft/monaco-editor/issues/1228 +https://github.com/microsoft/monaco-editor/issues/568#issuecomment-1499966160 \ No newline at end of file diff --git a/src/renderer/src/components/CodeEditor/utils.ts b/src/renderer/src/components/CodeEditor/utils.ts new file mode 100644 index 0000000..08b192b --- /dev/null +++ b/src/renderer/src/components/CodeEditor/utils.ts @@ -0,0 +1,32 @@ +export function judgeFile(filename: string) { + if (!filename) return + let ext = [ + { language: "vue", ext: ".vue", index: -1 }, + { language: "javascript", ext: ".js", index: -1 }, + { language: "css", ext: ".css", index: -1 }, + { language: "html", ext: ".html", index: -1 }, + { language: "tsx", ext: ".tsx", index: -1 }, + { language: "typescript", ext: ".ts", index: -1 }, + { language: "markdown", ext: ".md", index: -1 }, + { language: "json", ext: ".json", index: -1 }, + { language: "web", ext: ".web", index: -1 }, + { language: "dot", pre: ".", index: -1 }, + ] + let cur + for (let i = 0; i < ext.length; i++) { + const e = ext[i] + if (e.ext && filename.endsWith(e.ext)) { + let index = filename.lastIndexOf(e.ext) + e.index = index + cur = e + break + } + if (e.pre && filename.startsWith(e.pre)) { + let index = filename.indexOf(e.pre) + e.index = index + cur = e + break + } + } + return cur +} diff --git a/src/renderer/src/components/NavBar.vue b/src/renderer/src/components/NavBar.vue index 7a8c246..7ad502e 100644 --- a/src/renderer/src/components/NavBar.vue +++ b/src/renderer/src/components/NavBar.vue @@ -46,6 +46,7 @@ import { PopupMenu } from "@/bridge/PopupMenu" const router = useRouter() const route = useRoute() const isFullScreen = ref(false) + onBeforeMount(async () => { isFullScreen.value = await api.call("BasicCommand.isFullscreen") }) @@ -62,18 +63,18 @@ const isHome = computed(() => { function back() { router.push("/") } - +const { t } = useI18n() const onClickMenu = e => { const menu = new PopupMenu([ { - label: isFullScreen.value ? "取消全屏" : "全屏", + label: isFullScreen.value ? t("qu-xiao-quan-ping") : t("quan-ping"), async click() { await PlatForm.toggleFullScreen() isFullScreen.value = !isFullScreen.value }, }, { - label: "切换开发者工具", + label: t("qie-huan-kai-fa-zhe-gong-ju"), async click() { PlatForm.toggleDevTools() }, @@ -92,6 +93,7 @@ const onClickAbout = () => { .list { @apply: flex gap="5px"; -webkit-app-region: no-drag; + .item { @apply: text-sm px-2 hover:rounded-md hover:bg-gray-2 hover:cursor-pointer text="hover:hover"; } diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts new file mode 100644 index 0000000..ad1e772 --- /dev/null +++ b/src/renderer/src/i18n/index.ts @@ -0,0 +1,26 @@ +import { createI18n } from "vue-i18n" +import messages from "@intlify/unplugin-vue-i18n/messages" +import { datetimeFormats } from "locales" // 引入以便热更新同时提供datetimeFormats +// https://vue-i18n.intlify.dev/guide/essentials/syntax.html +// let locale = "zh" + +// const curConfig = useGetConfig() +// if (curConfig.value.language) { +// locale = curConfig.value.language +// } + +// console.log(locale) +console.log(messages) + +const i18n = createI18n({ + legacy: false, + allowComposition: true, + locale: "zh", + fallbackLocale: "zh", + messages: messages, + // @ts-ignore ... + datetimeFormats, +}) + +export { i18n } +export default i18n diff --git a/src/renderer/src/layouts/default.vue b/src/renderer/src/layouts/default.vue index f8c6000..c008a8a 100644 --- a/src/renderer/src/layouts/default.vue +++ b/src/renderer/src/layouts/default.vue @@ -7,3 +7,9 @@ import Simplebar from "simplebar-vue" + + diff --git a/src/renderer/src/main.ts b/src/renderer/src/main.ts index f4c487c..904ede1 100644 --- a/src/renderer/src/main.ts +++ b/src/renderer/src/main.ts @@ -7,7 +7,9 @@ import { createApp } from "vue" import App from "./App.vue" import router from "./router" +import i18n from "./i18n" const app = createApp(App) +app.use(i18n) app.use(router as any) app.mount("#app") diff --git a/src/renderer/src/pages/browser.vue b/src/renderer/src/pages/browser.vue new file mode 100644 index 0000000..3c53203 --- /dev/null +++ b/src/renderer/src/pages/browser.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/src/renderer/src/pages/index copy.vue b/src/renderer/src/pages/index copy.vue deleted file mode 100644 index c573878..0000000 --- a/src/renderer/src/pages/index copy.vue +++ /dev/null @@ -1,149 +0,0 @@ - - - - - diff --git a/src/renderer/src/pages/index.vue b/src/renderer/src/pages/index.vue index 00c6a90..c09e322 100644 --- a/src/renderer/src/pages/index.vue +++ b/src/renderer/src/pages/index.vue @@ -4,10 +4,28 @@ definePage({ home: true, }, }) + +const state = reactive({ + content: "", + name: "aaa.ts", +}) diff --git a/src/renderer/typed-router.d.ts b/src/renderer/typed-router.d.ts index 6ed6a79..b53fc20 100644 --- a/src/renderer/typed-router.d.ts +++ b/src/renderer/typed-router.d.ts @@ -21,6 +21,6 @@ declare module 'vue-router/auto-routes' { '/': RouteRecordInfo<'/', '/', Record, Record>, '/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue }, { all: ParamValue }>, 'about': RouteRecordInfo<'about', '/about', Record, Record>, - '/index copy': RouteRecordInfo<'/index copy', '/index copy', Record, Record>, + '/browser': RouteRecordInfo<'/browser', '/browser', Record, Record>, } } diff --git a/tsconfig.node.json b/tsconfig.node.json index b26fd2e..b022560 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,11 +1,11 @@ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", - "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "config/**/*", "src/types/**/*"], + "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "config/**/*", "src/types/**/*", "packages/locales/main.ts"], "compilerOptions": { "composite": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "types": ["electron-vite/node", "reflect-metadata"], + "types": ["electron-vite/node", "reflect-metadata",], "baseUrl": ".", "paths": { "#": ["src/types/index"], @@ -14,6 +14,7 @@ "config/*": ["config/*"], "main/*": ["src/main/*"], "res/*": ["resources/*"], + "locales/*": ["packages/locales/*"], } } }