Browse Source

feat: 增加很多功能

feat/icon
npmrun 1 month ago
parent
commit
b4b975174d
  1. 2
      .vscode/extensions.json
  2. 9
      .vscode/settings.json
  3. 15
      electron.vite.config.ts
  4. 6
      package.json
  5. 35
      packages/locales/index.ts
  6. 77
      packages/locales/languages/en.json
  7. 76
      packages/locales/languages/zh.json
  8. 39
      packages/locales/main.ts
  9. 12
      packages/locales/package.json
  10. 284
      pnpm-lock.yaml
  11. 11
      src/main/commands/BasicCommand.ts
  12. 479
      src/main/modules/zephyr/index.ts
  13. 2
      src/renderer/auto-imports.d.ts
  14. 1
      src/renderer/components.d.ts
  15. BIN
      src/renderer/src/components/CodeEditor/120x120.png
  16. 57
      src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts
  17. 1
      src/renderer/src/components/CodeEditor/a.d.ts
  18. 296
      src/renderer/src/components/CodeEditor/code-editor.vue
  19. 16
      src/renderer/src/components/CodeEditor/monaco.ts
  20. 3
      src/renderer/src/components/CodeEditor/readme.md
  21. 32
      src/renderer/src/components/CodeEditor/utils.ts
  22. 8
      src/renderer/src/components/NavBar.vue
  23. 26
      src/renderer/src/i18n/index.ts
  24. 6
      src/renderer/src/layouts/default.vue
  25. 2
      src/renderer/src/main.ts
  26. 10
      src/renderer/src/pages/browser.vue
  27. 20
      src/renderer/src/pages/index.vue
  28. 2
      src/renderer/typed-router.d.ts
  29. 5
      tsconfig.node.json

2
.vscode/extensions.json

@ -1,3 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
"recommendations": ["dbaeumer.vscode-eslint", "lokalise.i18n-ally"]
}

9
.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"]
}

15
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")
},
}),
],
},
})

6
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"
}
}

35
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 }

77
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"
}

76
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": "全屏"
}

39
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, Prefix extends string = ""> = T extends object
? {
[K in keyof T & (string | number)]: FlattenObject<T[K], Prefix extends "" ? `${K}` : `${Prefix}.${K}`>
}[keyof T & (string | number)]
: Prefix
type FlattenKeys<T> = FlattenObject<T>
type TranslationKey = FlattenKeys<typeof zh>
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 }

12
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"
}

284
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: {}

11
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()

479
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<string, boolean>()
// 访问频率限制相关
private readonly rateLimiter = new Map<string, number>()
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<boolean> {
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<boolean> {
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<typeof this.pathConfig>) {
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<boolean> {
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<Response> {
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<Response> {
const cleanup = async (error?: Error): Promise<Response> => {
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<string, string> = {
".txt": "text/plain",
".json": "application/json",
".md": "text/markdown",
}
return contentTypes[ext] || "application/octet-stream"
}
private async isSymlink(filePath: string): Promise<boolean> {
try {
const stats = await fs.promises.lstat(filePath)
return stats.isSymbolicLink()
} catch {
return false
}
}
private async checkFileSize(filePath: string): Promise<boolean> {
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<Response> {
const cleanup = async (error?: Error): Promise<Response> => {
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)
}
}
}

2
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<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>

1
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']

BIN
src/renderer/src/components/CodeEditor/120x120.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

57
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)
}
}

1
src/renderer/src/components/CodeEditor/a.d.ts

@ -0,0 +1 @@
type A = string

296
src/renderer/src/components/CodeEditor/code-editor.vue

@ -0,0 +1,296 @@
<script lang="ts" setup>
import { judgeFile } from "./utils"
import { monaco } from "./monaco"
import { computed, getCurrentScope, onBeforeUnmount, onMounted, onScopeDispose, ref, watch } from "vue"
import DefaultLogo from "./120x120.png"
import { PlaceholderContentWidget } from "./PlaceholderContentWidget"
const editorRef = ref<HTMLDivElement>()
let editor: monaco.editor.IStandaloneCodeEditor | null = null
let placeholderWidget: PlaceholderContentWidget | null = null
const props = withDefaults(
defineProps<{
modelValue?: string
name?: string
logoType?: "bg" | "logo"
logo?: string
placeholder?: string
fontFamily?: string
readonly?: boolean
}>(),
{
logo: DefaultLogo,
readonly: false,
logoType: "logo",
modelValue: "",
name: "",
},
)
const emit = defineEmits<{
(e: "update:modelValue", code: string): void
(e: "change", code: string): void
(e: "cursor:position", position: [number, number]): void
}>()
defineExpose({
scrollTop() {
editor?.setScrollTop(0)
},
insertText(text: string, type = "cursor") {
if (editor) {
const m = editor.getModel()
const currentPosition = editor.getPosition()
if (m) {
console.log(currentPosition)
if (type === "cursor" && currentPosition) {
m.pushEditOperations(
[],
[
{
range: new monaco.Range(
currentPosition.lineNumber,
currentPosition.column,
currentPosition.lineNumber,
currentPosition.column,
),
text,
},
],
() => [
new monaco.Selection(
currentPosition.lineNumber,
currentPosition.column,
currentPosition.lineNumber,
currentPosition.column,
),
],
)
} else {
const lineCount = m.getLineCount()
const lastLineLength = m.getLineLength(lineCount)
const range = new monaco.Selection(lineCount, lastLineLength + 1, lineCount, lastLineLength + 1)
const text = "your text"
const op = {
range: range,
text: text,
}
m.pushEditOperations([], [op], () => [range])
}
}
}
},
setContent(content: string) {
if (editorRef.value && editor) {
editor.setValue(content)
}
},
})
let isInnerChange = false
function updateModel(name: string, content: string) {
if (editor) {
const oldModel = editor.getModel() //
const file = judgeFile(name)
// model
// monaco.editor.createModel("const a = 111","typescript", monaco.Uri.parse('file://root/file3.ts'))
const model: monaco.editor.ITextModel = monaco.editor.createModel(content ?? "", file?.language ?? "txt")
model.onDidChangeContent(() => {
if (model) {
isInnerChange = true
const code = model.getValue()
emit("update:modelValue", code)
emit("change", code)
console.log(343)
}
})
if (oldModel) {
oldModel.dispose()
}
editor.setModel(model)
}
}
function resizeLayout() {
if (editor) {
editor.layout()
}
}
onMounted(() => {
if (editorRef.value && !editor) {
editor = monaco.editor.create(editorRef.value, {
theme: "vs-light",
fontFamily: props.fontFamily ?? "Cascadia Mono, Consolas, 'Courier New', monospace",
readOnly: props.readonly,
minimap: {
autohide: true,
},
}) as monaco.editor.IStandaloneCodeEditor
editor.onDidChangeCursorPosition(e => {
emit("cursor:position", [e.position.lineNumber, e.position.column])
})
editorRef.value.addEventListener("resize", resizeLayout)
}
watch(
() => props.placeholder,
() => {
if (editor) {
if (placeholderWidget) {
placeholderWidget.dispose()
placeholderWidget = null
}
if (props.placeholder) {
placeholderWidget = new PlaceholderContentWidget(props.placeholder, editor)
}
}
},
{
immediate: true,
},
)
//
watch(
() => props.modelValue,
async str => {
if (editor && !isInnerChange) {
editor.setValue(str)
} else {
isInnerChange = false
}
},
{ immediate: true },
)
watch(
() => props.name,
async name => {
if (editor) {
updateModel(name, props.modelValue)
}
},
{ immediate: true },
)
watch(
() => props.readonly,
() => {
if (editor) {
editor.updateOptions({
readOnly: props.readonly,
})
}
},
)
watch(
() => props.fontFamily,
() => {
if (editor) {
editor.updateOptions({
fontFamily: props.fontFamily,
})
}
},
)
})
onBeforeUnmount(() => {
if (editorRef.value) {
editorRef.value.removeEventListener("resize", resizeLayout)
}
if (editor) {
const oldModel = editor.getModel()
if (oldModel) {
oldModel.dispose()
}
editor?.dispose()
editor = null
console.log("editor dispose")
}
})
const style = computed(() => {
if (props.logo && props.logoType === "bg") {
return {
backgroundImage: `url(${props.logo})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
}
}
return {}
})
const getLogo = computed(() => {
if (props.logo) return props.logo
return DefaultLogo
})
function useResizeObserver(callback: ResizeObserverCallback) {
const isSupported = window && "ResizeObserver" in window
let observer: ResizeObserver | undefined
const cleanup = () => {
if (observer) {
observer.disconnect()
observer = undefined
}
}
const stopWatch = watch(
() => editorRef.value,
el => {
cleanup()
if (isSupported && window && el) {
observer = new ResizeObserver(callback)
observer!.observe(el, {})
}
},
{ immediate: true },
)
const stop = () => {
cleanup()
stopWatch()
}
function tryOnScopeDispose(fn: () => void) {
if (getCurrentScope()) {
onScopeDispose(fn)
return true
}
return false
}
tryOnScopeDispose(() => {
stop()
})
}
useResizeObserver(() => {
if (editor) {
editor.layout()
}
})
</script>
<template>
<div class="monaco-wrapper">
<div class="monaco-editor" ref="editorRef"></div>
<div class="monaco-bg" :style="style">
<img v-if="logoType === 'logo' && getLogo" class="monaco-logo" :src="getLogo" alt="" />
</div>
</div>
</template>
<style lang="scss" scoped>
.monaco-wrapper {
height: 100%;
position: relative;
.monaco-editor {
height: 100%;
}
.monaco-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
opacity: 0.1;
overflow: hidden;
.monaco-logo {
@apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2;
}
}
}
</style>

16
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 }

3
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

32
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
}

8
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";
}

26
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

6
src/renderer/src/layouts/default.vue

@ -7,3 +7,9 @@ import Simplebar from "simplebar-vue"
<RouterView></RouterView>
</Simplebar>
</template>
<style scoped>
:deep(.simplebar-content) {
height: 100%;
}
</style>

2
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")

10
src/renderer/src/pages/index copy.vue → 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<string, any> = 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,

20
src/renderer/src/pages/index.vue

@ -4,10 +4,28 @@ definePage({
home: true,
},
})
const state = reactive({
content: "",
name: "aaa.ts",
})
</script>
<template>
<div class="locale-changer">
<select v-model="$i18n.locale">
<option v-for="locale in $i18n.availableLocales" :key="`locale-${locale}`" :value="locale">{{ locale }}</option>
</select>
</div>
<div @click="$router.push('/about')">
<div v-for="i in 1000">{{ i }}</div>
<span>跳转 about</span>
</div>
<div @click="$router.push('/browser')">
<span>跳转 browser</span>
</div>
<input v-model="state.content" />
<CodeEditor v-model="state.content" style="height: 400px" :name="state.name" placeholder="输入代码"></CodeEditor>
<div v-for="i in 1000" :key="i">
<span>{{ i }}</span>
</div>
</template>

2
src/renderer/typed-router.d.ts

@ -21,6 +21,6 @@ declare module 'vue-router/auto-routes' {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
'about': RouteRecordInfo<'about', '/about', Record<never, never>, Record<never, never>>,
'/index copy': RouteRecordInfo<'/index copy', '/index copy', Record<never, never>, Record<never, never>>,
'/browser': RouteRecordInfo<'/browser', '/browser', Record<never, never>, Record<never, never>>,
}
}

5
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/*"],
}
}
}

Loading…
Cancel
Save