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 0000000..db0a963 Binary files /dev/null and b/src/renderer/src/components/CodeEditor/120x120.png differ diff --git a/src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts b/src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts new file mode 100644 index 0000000..bc1861b --- /dev/null +++ b/src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts @@ -0,0 +1,57 @@ +import { monaco } from "./monaco" + +/** + * Represents an placeholder renderer for monaco editor + * Roughly based on https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts + */ +export class PlaceholderContentWidget implements monaco.editor.IContentWidget { + private static readonly ID = "editor.widget.placeholderHint" + + private domNode: HTMLElement | undefined + + constructor( + private readonly placeholder: string, + private readonly editor: monaco.editor.ICodeEditor, + ) { + // register a listener for editor code changes + editor.onDidChangeModelContent(() => 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/index copy.vue b/src/renderer/src/pages/browser.vue similarity index 97% rename from src/renderer/src/pages/index copy.vue rename to src/renderer/src/pages/browser.vue index c573878..3c53203 100644 --- a/src/renderer/src/pages/index copy.vue +++ b/src/renderer/src/pages/browser.vue @@ -2,17 +2,11 @@ import Simplebar from "simplebar-vue" import { getAssetsFile } from "@/utils" -definePage({ - meta: { - home: true, - }, -}) - const allModules: Record = import.meta.glob("./_ui/**/*.vue", { eager: true }) let allApp: any[] = [] Object.keys(allModules).forEach(key => { - let [_1, p] = key.match("\.\/_ui\/(.*?)\.vue")! - p = p.replace(/\.vue$/, "") + // let [, p] = key.match("./_ui/(.*?).vue")! + // p = p.replace(/\.vue$/, "") const m = allModules[key]?.default || allModules[key] allApp.push({ label: m.title, 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/*"], } } }