Compare commits

...

23 Commits

Author SHA1 Message Date
npmrun f5c6bf2432 feat: 添加vscode-icons-js依赖并在index.vue中使用图标 2 weeks ago
npmrun f4505621d5 lfs管理icon 2 weeks ago
谢亚昕 6692e16720 优化 3 weeks ago
谢亚昕 de5f511d6a fix bug 3 weeks ago
npmrun d4be8b22b9 feat(崩溃处理): 添加崩溃处理模块以捕获并报告应用崩溃 4 weeks ago
npmrun c142937af9 refactor(logger): 重构错误处理逻辑,分离渲染进程和preload进程的错误处理 4 weeks ago
npmrun 950bfe9060 feat(logger): 添加错误处理模块并集成到主进程和渲染进程 4 weeks ago
npmrun 7035429775 feat(logger): 添加固定命名空间的日志记录功能 4 weeks ago
npmrun 05f83e2a08 feat: 添加logger和setting模块并重构日志系统 4 weeks ago
npmrun fa6ef80493 Merge branch 'feat/优化' of ssh://git.xieyaxin.top:8892/topuser/electron-app into feat/优化 4 weeks ago
npmrun 0f093b2ef9 refactor(PlatForm): 重构 PlatForm 相关代码并添加重载功能 4 weeks ago
谢亚昕 80cc4fe0fe fix bug 4 weeks ago
npmrun 28eea56a3d refactor(config): 导出 IDefaultConfig 接口以重用类型定义 4 weeks ago
npmrun b6964f5fbe style: 统一代码缩进为2个空格,提升代码可读性 4 weeks ago
npmrun 7246ab2d9a refactor(命令模块): 将命令模块从主进程迁移到通用模块,并重构相关代码 4 weeks ago
npmrun bd9ac214c6 feat(updater): 实现热更新功能并优化命令处理 4 weeks ago
谢亚昕 dcdc4aa857 fix bug 1 month ago
npmrun 2d5a57853d feat 1 month ago
谢亚昕 3c434df31c 修改了一些东西 1 month ago
npmrun b4b975174d feat: 增加很多功能 2 months ago
npmrun 91f06eb4a1 feat: 优化 2 months ago
谢亚昕 248716be69 feat: 优化界面 2 months ago
npmrun ca363ceac9 feat: 优化 2 months ago
  1. 2
      .editorconfig
  2. 47
      .eslintrc.cjs
  3. 1
      .gitattributes
  4. 41
      .prettierrc
  5. 2
      .vscode/extensions.json
  6. 74
      .vscode/launch.json
  7. 32
      .vscode/settings.json
  8. 76
      config/index.ts
  9. 62
      electron-builder.yml
  10. 166
      electron.vite.config.ts
  11. 148
      package.json
  12. 41
      packages/locales/index.ts
  13. 20
      packages/locales/languages/en.json
  14. 20
      packages/locales/languages/zh.json
  15. 53
      packages/locales/main.ts
  16. 7
      packages/locales/package.json
  17. 31
      packages/logger/common.ts
  18. 486
      packages/logger/crash-handler.ts
  19. 177
      packages/logger/main-error.ts
  20. 275
      packages/logger/main.ts
  21. 7
      packages/logger/package.json
  22. 195
      packages/logger/preload-error.ts
  23. 153
      packages/logger/preload.ts
  24. 243
      packages/logger/renderer-error.ts
  25. 230
      packages/setting/main.ts
  26. 7
      packages/setting/package.json
  27. 391
      pnpm-lock.yaml
  28. 2
      pnpm-workspace.yaml
  29. 17
      resources/fuck.html
  30. 17
      src/common/_ioc.main.ts
  31. 5
      src/common/event/PlatForm/hook.ts
  32. 48
      src/common/event/PlatForm/index.ts
  33. 120
      src/common/event/PlatForm/main/command.ts
  34. 54
      src/common/event/Tabs/index.ts
  35. 65
      src/common/event/Tabs/main/command.ts
  36. 5
      src/common/event/common.ts
  37. 14
      src/common/event/update/index.ts
  38. 10
      src/common/event/update/main/command.ts
  39. 8
      src/common/event/update/main/index.ts
  40. 12
      src/common/lib/_Base.ts
  41. 53
      src/common/lib/abstract.ts
  42. 29
      src/common/lib/browser.ts
  43. 20
      src/common/lib/electron.ts
  44. 278
      src/main/App copy.ts
  45. 127
      src/main/App.ts
  46. 8
      src/main/_ioc.ts
  47. 24
      src/main/_iocClass.ts
  48. 6
      src/main/base/base.ts
  49. 63
      src/main/commands/BasicCommand.ts
  50. 66
      src/main/commands/TabsCommand.ts
  51. 15
      src/main/commands/_ioc.ts
  52. 30
      src/main/controller/BasicService.ts
  53. 54
      src/main/controller/TabsService.ts
  54. 6
      src/main/controller/_ioc.ts
  55. 90
      src/main/index.ts
  56. 37
      src/main/modules/_ioc.ts
  57. 152
      src/main/modules/api/index.ts
  58. 1
      src/main/modules/api/readme.md
  59. 1
      src/main/modules/api/test.ts
  60. 159
      src/main/modules/commands/index.ts
  61. 44
      src/main/modules/db/custom.ts
  62. 120
      src/main/modules/db/index.ts
  63. 235
      src/main/modules/setting/index.ts
  64. 8
      src/main/modules/tabs/Constant.ts
  65. 486
      src/main/modules/tabs/Tab.ts
  66. 204
      src/main/modules/tabs/index.ts
  67. 118
      src/main/modules/updater/hot/index.ts
  68. 203
      src/main/modules/updater/index.ts
  69. 612
      src/main/modules/window-manager/index.ts
  70. 201
      src/main/modules/window-manager/windowsMap.ts
  71. 497
      src/main/modules/zephyr/index.ts
  72. 34
      src/main/utils/index.ts
  73. 106
      src/preload/call.ts
  74. 8
      src/preload/index.d.ts
  75. 88
      src/preload/index.ts
  76. 50
      src/renderer/about.html
  77. 2
      src/renderer/auto-imports.d.ts
  78. 1
      src/renderer/components.d.ts
  79. 27
      src/renderer/index.html
  80. BIN
      src/renderer/public/icons/default_file.svg
  81. BIN
      src/renderer/public/icons/default_folder.svg
  82. BIN
      src/renderer/public/icons/default_folder_opened.svg
  83. BIN
      src/renderer/public/icons/default_root_folder.svg
  84. BIN
      src/renderer/public/icons/default_root_folder_opened.svg
  85. BIN
      src/renderer/public/icons/file_type_access.svg
  86. BIN
      src/renderer/public/icons/file_type_access2.svg
  87. BIN
      src/renderer/public/icons/file_type_actionscript.svg
  88. BIN
      src/renderer/public/icons/file_type_actionscript2.svg
  89. BIN
      src/renderer/public/icons/file_type_ada.svg
  90. BIN
      src/renderer/public/icons/file_type_advpl.svg
  91. BIN
      src/renderer/public/icons/file_type_affectscript.svg
  92. BIN
      src/renderer/public/icons/file_type_affinitydesigner.svg
  93. BIN
      src/renderer/public/icons/file_type_affinityphoto.svg
  94. BIN
      src/renderer/public/icons/file_type_affinitypublisher.svg
  95. BIN
      src/renderer/public/icons/file_type_agda.svg
  96. BIN
      src/renderer/public/icons/file_type_ai.svg
  97. BIN
      src/renderer/public/icons/file_type_ai2.svg
  98. BIN
      src/renderer/public/icons/file_type_al.svg
  99. BIN
      src/renderer/public/icons/file_type_al_dal.svg
  100. BIN
      src/renderer/public/icons/file_type_allcontributors.svg

2
.editorconfig

@ -4,7 +4,7 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

47
.eslintrc.cjs

@ -1,34 +1,23 @@
const { readFileSync, readFile } = require("node:fs")
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
const prettierConfig = JSON.parse(readFileSync("./.prettierrc.json", { encoding: "utf-8" }))
module.exports = {
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"@electron-toolkit",
"@electron-toolkit/eslint-config-ts/eslint-recommended",
"@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier",
],
rules: {
"vue/require-default-prop": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-explicit-any": "off",
"prettier/prettier": [
"error",
{
tabWidth: 4,
useTabs: false,
semi: false,
singleQuote: false,
trailingComma: "all",
bracketSpacing: true,
arrowParens: "avoid",
printWidth: 140,
htmlWhitespaceSensitivity: "ignore",
proseWrap: "preserve",
endOfLine: "auto",
},
],
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"@electron-toolkit",
"@electron-toolkit/eslint-config-ts/eslint-recommended",
"@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier",
],
rules: {
"vue/require-default-prop": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-explicit-any": "off",
"prettier/prettier": ["error", prettierConfig],
},
}

1
.gitattributes

@ -0,0 +1 @@
src/renderer/public/icons/*.svg filter=lfs diff=lfs merge=lfs -text

41
.prettierrc

@ -1,13 +1,32 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": false,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 140,
"htmlWhitespaceSensitivity": "ignore",
"proseWrap": "preserve",
"endOfLine": "auto"
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 140,
"htmlWhitespaceSensitivity": "ignore",
"proseWrap": "preserve",
"endOfLine": "auto",
"vueIndentScriptAndStyle": true,
"embeddedLanguageFormatting": "auto",
"jsxSingleQuote": false,
"jsxBracketSameLine": false,
"quoteProps": "as-needed",
"overrides": [
{
"files": "*.json",
"options": {
"printWidth": 80
}
},
{
"files": ["*.vue", "*.tsx"],
"options": {
"singleAttributePerLine": false
}
}
]
}

2
.vscode/extensions.json

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

74
.vscode/launch.json

@ -1,39 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}

32
.vscode/settings.json

@ -1,11 +1,25 @@
{
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[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"],
"i18n-ally.translate.engines":[
"openai"
],
// "i18n-ally.translate.openai.apiKey": "",
"i18n-ally.translate.openai.apiModel": "Qwen/Qwen2.5-72B-Instruct",
"i18n-ally.translate.openai.apiRoot": "https://api.siliconflow.cn",
"i18n-ally.translate.overrideExisting": true
}

76
config/index.ts

@ -1,35 +1,47 @@
import { LogLevel } from "logger/common"
// 定义主题类型
type ThemeType = "light" | "dark" | "auto"
// 定义语言类型
type LanguageType = "zh" | "en"
// 定义编辑器logo类型
type LogoType = "logo" | "bg"
// 配置接口定义
export interface IDefaultConfig {
language: LanguageType
"common.theme": ThemeType
"debug": LogLevel,
"desktop:wallpaper": string
"update.repo"?: string
"update.owner"?: string
"update.allowDowngrade": boolean
"update.allowPrerelease": boolean
"editor.bg": string
"editor.logoType": LogoType
"editor.fontFamily": string
storagePath: string
}
interface IConfig {
app_title: string
default_config: {
language: "zh" | "en" // i18n
"common.theme": "light" | "dark" | "auto" // 主题
"desktop:wallpaper": string
"update.repo"?: string // 更新地址
"update.owner"?: string // 更新通道
"update.allowDowngrade": boolean
"update.allowPrerelease": boolean
"editor.bg": string // 更新通道
"editor.logoType": "logo" | "bg" // 更新通道
"editor.fontFamily": string // 更新通道
// "snippet.storagePath": string // 代码片段保存位置
// "bookmark.storagePath": string // 书签保存位置
// backup_rule: string // 备份规则
storagePath: string // 存储地址
}
app_title: string
default_config: IDefaultConfig
}
// 默认配置导出
export default {
app_title: "zephyr", // 和风
default_config: {
storagePath: "$storagePath$",
language: "zh",
"common.theme": "auto",
"desktop:wallpaper": "",
"editor.bg": "",
"editor.logoType": "logo",
"editor.fontFamily": "Cascadia Mono, Consolas, 'Courier New', monospace",
"update.repo": "wood-desktop",
"update.owner": "npmrun",
"update.allowDowngrade": false,
"update.allowPrerelease": false,
},
} as IConfig
app_title: "zephyr", // 和风
default_config: {
storagePath: "$storagePath$",
language: "zh",
debug: LogLevel.INFO,
"common.theme": "auto",
"desktop:wallpaper": "",
"editor.bg": "",
"editor.logoType": "logo",
"editor.fontFamily": "Cascadia Mono, Consolas, 'Courier New', monospace",
"update.repo": "wood-desktop",
"update.owner": "npmrun",
"update.allowDowngrade": false,
"update.allowPrerelease": false,
},
} as const satisfies IConfig

62
electron-builder.yml

@ -1,45 +1,45 @@
appId: com.zephyr.app
productName: zephyr
directories:
buildResources: build
buildResources: build
files:
- "!**/.vscode/*"
- "!src/*"
- "!electron.vite.config.{js,ts,mjs,cjs}"
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
- "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}"
- "!**/.vscode/*"
- "!src/*"
- "!electron.vite.config.{js,ts,mjs,cjs}"
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
- "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}"
asarUnpack:
- resources/**
- resources/**
win:
executableName: zephyr
executableName: zephyr
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
provider: generic
url: https://example.com/auto-updates
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/
mirror: https://npmmirror.com/mirrors/electron/

166
electron.vite.config.ts

@ -9,86 +9,104 @@ 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: {
resolve: {
alias: {
config: resolve("config"),
main: resolve("src/main"),
res: resolve("resources"),
},
},
plugins: [externalizeDepsPlugin()],
main: {
resolve: {
alias: {
config: resolve("config"),
main: resolve("src/main"),
common: resolve("src/common"),
"@res": resolve("resources"),
},
},
preload: {
plugins: [externalizeDepsPlugin()],
plugins: [externalizeDepsPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()],
},
renderer: {
root: resolve(__dirname, "./src/renderer"),
resolve: {
alias: {
config: resolve("config"),
common: resolve("src/common"),
"@": resolve("src/renderer/src"),
"@res": resolve("resources"),
},
},
renderer: {
root: resolve(__dirname, "./src/renderer"),
resolve: {
alias: {
config: resolve("config"),
"@": resolve("src/renderer/src"),
"@res": resolve("resources"),
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/assets/style/global" as *;\n`,
api: "modern-compiler",
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/assets/style/global" as *;\n`,
},
},
},
},
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "./src/renderer/index.html"),
about: resolve(__dirname, "./src/renderer/about.html"),
},
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "./src/renderer/index.html"),
about: resolve(__dirname, "./src/renderer/about.html"),
},
},
},
},
plugins: [
UnoCSS(),
VueMacros({
plugins: {
vue: vue(),
vueJsx: vueJsx(),
vueRouter: VueRouter({
root: resolve(__dirname, "src/renderer"),
// https://github.com/posva/unplugin-vue-router
extensions: [".vue", ".setup.tsx"],
exclude: ["**/_ui"],
}),
},
plugins: [
UnoCSS(),
VueMacros({
plugins: {
vue: vue(),
vueJsx: vueJsx(),
vueRouter: VueRouter({
root: resolve(__dirname, "src/renderer"),
// https://github.com/posva/unplugin-vue-router
extensions: [".vue", ".setup.tsx"],
exclude: ["**/_ui"],
}),
},
}),
Layouts({
layoutsDirs: "src/layouts",
pagesDirs: "src/pages",
defaultLayout: "default",
extensions: ["vue", "setup.tsx"],
exclude: ["**/_ui"],
}),
// https://github.com/antfu/unplugin-auto-import
AutoImport({
imports: [
"vue",
"@vueuse/core",
VueRouterAutoImports,
{
// add any other imports you were relying on
"vue-router/auto": ["useLink"],
},
],
dts: true,
dirs: ["src/composables"],
vueTemplate: true,
}),
// https://github.com/antfu/vite-plugin-components
Components({
dts: true,
dirs: ["src/components"],
}),
}),
VueI18nPlugin({
compositionOnly: false,
include: resolve(__dirname, "packages/locales/languages/**"),
}),
Layouts({
layoutsDirs: "src/layouts",
pagesDirs: "src/pages",
defaultLayout: "default",
extensions: ["vue", "setup.tsx"],
exclude: ["**/_ui"],
}),
// https://github.com/antfu/unplugin-auto-import
AutoImport({
imports: [
"vue",
"@vueuse/core",
VueRouterAutoImports,
{
// add any other imports you were relying on
"vue-router/auto": ["useLink"],
},
"vue-i18n",
],
},
dts: true,
dirs: ["src/composables"],
vueTemplate: true,
}),
// https://github.com/antfu/vite-plugin-components
Components({
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")
},
}),
],
},
})

148
package.json

@ -1,71 +1,81 @@
{
"name": "zephyr",
"type": "module",
"private": true,
"version": "1.0.0",
"description": "An Electron application with Vue and TypeScript",
"main": "./out/main/index.js",
"author": "example.com",
"homepage": "https://electron-vite.org",
"scripts": {
"runInstall": "node node_modules/electron/install.js",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "chcp 65001 && set DEBUG=app:*&& electron-vite dev",
"dev:watch": "chcp 65001 & set DEBUG=app:*& electron-vite dev --watch",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@types/debug": "^4.1.12",
"@unocss/reset": "^0.64.1",
"@vueuse/core": "^12.7.0",
"electron-updater": "^6.3.9",
"inversify": "^6.2.2",
"lowdb": "^7.0.1",
"reflect-metadata": "^0.2.2",
"sass": "^1.85.0",
"unplugin-auto-import": "^19.1.0",
"unplugin-vue-components": "^28.4.0",
"unplugin-vue-macros": "^2.14.2",
"unplugin-vue-router": "^0.11.2",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@rushstack/eslint-patch": "^1.10.5",
"@types/node": "^20.17.19",
"@unocss/preset-rem-to-px": "^0.64.1",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"debug": "^4.4.0",
"electron": "^31.7.7",
"electron-builder": "^24.13.3",
"electron-vite": "^2.3.0",
"eslint": "^8.57.1",
"eslint-plugin-vue": "^9.32.0",
"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-vue-layouts": "^0.11.0",
"vue": "^3.5.13",
"vue-tsc": "^2.1.10"
}
"name": "zephyr",
"type": "module",
"private": true,
"version": "0.0.1",
"description": "An Electron application with Vue and TypeScript",
"main": "./out/main/index.js",
"author": "example.com",
"homepage": "https://electron-vite.org",
"scripts": {
"runInstall": "node node_modules/electron/install.js",
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "chcp 65001 && set DEBUG=app:*&& electron-vite dev",
"dev:watch": "chcp 65001 & set DEBUG=app:*& electron-vite dev --watch",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"electron-updater": "^6.3.9",
"inversify": "^6.2.2",
"lowdb": "^7.0.1",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
"@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/debug": "^4.1.12",
"@types/node": "^20.17.19",
"@unocss/preset-rem-to-px": "^0.64.1",
"@unocss/reset": "^0.64.1",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vueuse/core": "^12.7.0",
"debug": "^4.4.0",
"electron": "^31.7.7",
"electron-builder": "^24.13.3",
"electron-vite": "^2.3.0",
"eslint": "^8.57.1",
"eslint-plugin-vue": "^9.32.0",
"extract-zip": "^2.0.1",
"locales": "workspace:*",
"lodash-es": "^4.17.21",
"logger": "workspace:^",
"monaco-editor": "^0.52.2",
"prettier": "^3.5.1",
"rotating-file-stream": "^3.2.6",
"sass": "^1.85.0",
"setting": "workspace:^",
"simplebar-vue": "^2.4.0",
"typescript": "^5.7.3",
"unocss": "^0.64.1",
"unplugin-auto-import": "^19.1.0",
"unplugin-vue-components": "^28.4.0",
"unplugin-vue-macros": "^2.14.2",
"unplugin-vue-router": "^0.11.2",
"vite": "^5.4.14",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-vue-layouts": "^0.11.0",
"vscode-icons-js": "^11.6.1",
"vue": "^3.5.13",
"vue-i18n": "^11.1.1",
"vue-router": "^4.5.0",
"vue-tsc": "^2.1.10"
}
}

41
packages/locales/index.ts

@ -0,0 +1,41 @@
if (import.meta.env.DEV) {
// 引入之后可以热更新
import("./languages/zh.json")
import("./languages/en.json")
}
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 }

20
packages/locales/languages/en.json

@ -0,0 +1,20 @@
{
"update": {
"ready": {
"hot": {
"desc": "The new version v{version} is ready for update and will be automatically updated the next time you launch the program.",
"title": "Prompt"
}
}
},
"browser": {
"navbar": {
"menu": {
"fullscreen": "Full screen",
"quit-fullscreen": "Exit Full Screen",
"toggleDevTools": "Developer panel",
"label": "Menu"
}
}
}
}

20
packages/locales/languages/zh.json

@ -0,0 +1,20 @@
{
"update": {
"ready": {
"hot": {
"title": "提示",
"desc": "新版本 v{version} 已经准备好更新, 下次启动程序即可自动更新"
}
}
},
"browser": {
"navbar": {
"menu": {
"label": "菜单",
"fullscreen": "全屏",
"quit-fullscreen": "取消全屏",
"toggleDevTools": "开发者面板"
}
}
}
}

53
packages/locales/main.ts

@ -0,0 +1,53 @@
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 = "zh"
constructor() {
try {
this.locale = app.getLocale()
} catch (e) {
console.log(e)
}
}
isCN(): boolean {
return this.locale.startsWith("zh")
}
t(key: TranslationKey, replacements?: Record<string, string>): string {
let text: string = this.isCN() ? get(zh, key) : get(en, key)
if (!text) {
text = get(zh, key)
if (!text) {
return key
}
}
if (replacements) {
// 替换所有形如 {key} 的占位符
Object.entries(replacements).forEach(([key, value]) => {
console.log(text)
text = text.replace(new RegExp(`{${key}}`, "g"), value)
})
}
return text
}
}
const Locales = new Locale()
export default Locales
export { Locales }

7
packages/locales/package.json

@ -0,0 +1,7 @@
{
"name": "locales",
"version": "1.0.0",
"keywords": [],
"author": "",
"license": "ISC"
}

31
packages/logger/common.ts

@ -0,0 +1,31 @@
// 日志级别定义
export enum LogLevel {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
FATAL = 5,
OFF = 6,
}
// 日志级别名称映射
export const LogLevelName: Record<LogLevel, string> = {
[LogLevel.TRACE]: "TRACE",
[LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO",
[LogLevel.WARN]: "WARN",
[LogLevel.ERROR]: "ERROR",
[LogLevel.FATAL]: "FATAL",
[LogLevel.OFF]: "OFF",
}
// 日志颜色映射(控制台输出用)
export const LogLevelColor: Record<LogLevel, string> = {
[LogLevel.TRACE]: "\x1b[90m", // 灰色
[LogLevel.DEBUG]: "\x1b[36m", // 青色
[LogLevel.INFO]: "\x1b[32m", // 绿色
[LogLevel.WARN]: "\x1b[33m", // 黄色
[LogLevel.ERROR]: "\x1b[31m", // 红色
[LogLevel.FATAL]: "\x1b[35m", // 紫色
[LogLevel.OFF]: "", // 无色
}

486
packages/logger/crash-handler.ts

@ -0,0 +1,486 @@
import { app, dialog } from "electron"
import fs from "fs"
import path from "path"
import os from "os"
import logger from "./main"
import errorHandler, { ErrorDetail } from "./main-error"
/**
*
*/
export interface CrashReport {
timestamp: string
error: ErrorDetail
systemInfo: {
platform: string
release: string
arch: string
totalMemory: number
freeMemory: number
uptime: number
}
appInfo: {
version: string
name: string
path: string
argv: string[]
}
}
/**
*
*/
export interface CrashHandlerOptions {
crashReportDir?: string
maxReports?: number
showDialog?: boolean
}
/**
*
*/
const DEFAULT_OPTIONS: CrashHandlerOptions = {
maxReports: 10,
showDialog: true,
}
/**
*
*/
export class CrashHandler {
private static instance: CrashHandler
private options: CrashHandlerOptions
private crashReportDir: string
private initialized: boolean = false
private startTime: number = Date.now()
// private normalShutdown: boolean = false
/**
*
*/
public static getInstance(): CrashHandler {
if (!CrashHandler.instance) {
CrashHandler.instance = new CrashHandler()
}
return CrashHandler.instance
}
/**
*
*/
private constructor() {
this.options = { ...DEFAULT_OPTIONS }
this.crashReportDir = path.join(app.getPath("userData"), "crash-reports")
}
/**
*
*/
public init(options?: CrashHandlerOptions): void {
if (this.initialized) {
return
}
this.options = { ...this.options, ...options }
if (options?.crashReportDir) {
this.crashReportDir = options.crashReportDir
}
// 确保崩溃报告目录存在
if (!fs.existsSync(this.crashReportDir)) {
fs.mkdirSync(this.crashReportDir, { recursive: true })
}
// 检查上次是否崩溃
this.checkPreviousCrash()
// 记录应用启动时间
this.startTime = Date.now()
this.saveStartupMarker()
// 设置全局未捕获异常处理器
this.setupGlobalHandlers()
// 设置应用退出处理
app.on("before-quit", () => {
// this.normalShutdown = true
this.clearStartupMarker()
})
this.initialized = true
logger.info("crash-handler", "Crash handler initialized")
}
/**
*
*/
private setupGlobalHandlers(): void {
// 增强现有的错误处理器
const originalCaptureError = errorHandler.captureError.bind(errorHandler)
errorHandler.captureError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => {
// 调用原始方法记录错误
originalCaptureError(error, componentInfo, additionalInfo)
// 对于严重错误,生成崩溃报告
if (error instanceof Error && error.stack) {
this.generateCrashReport(error, componentInfo, additionalInfo)
}
}
// 捕获未处理的Promise异常
process.on("unhandledRejection", (reason, promise) => {
logger.error("crash-handler", `Unhandled Promise Rejection: ${reason}`)
this.generateCrashReport(reason, "unhandledRejection", { promise: String(promise) })
})
// 捕获未捕获的异常
process.on("uncaughtException", error => {
logger.error("crash-handler", `Uncaught Exception: ${error.message}`)
this.generateCrashReport(error, "uncaughtException")
// 显示错误对话框
if (this.options.showDialog) {
this.showCrashDialog(error)
}
})
}
/**
*
*/
private generateCrashReport(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void {
try {
// 格式化错误信息
const errorDetail = this.formatError(error, componentInfo, additionalInfo)
// 创建崩溃报告
const crashReport: CrashReport = {
timestamp: new Date().toISOString(),
error: errorDetail,
systemInfo: {
platform: os.platform(),
release: os.release(),
arch: os.arch(),
totalMemory: os.totalmem(),
freeMemory: os.freemem(),
uptime: os.uptime(),
},
appInfo: {
version: app.getVersion(),
name: app.getName(),
path: app.getAppPath(),
argv: process.argv,
},
}
// 保存崩溃报告
this.saveCrashReport(crashReport)
} catch (e) {
logger.error("crash-handler", `Failed to generate crash report: ${e}`)
}
}
/**
*
*/
private formatError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): ErrorDetail {
// 基本错误信息
const errorDetail: ErrorDetail = {
message: "",
timestamp: new Date().toISOString(),
type: "Unknown",
}
// 处理不同类型的错误
if (error instanceof Error) {
errorDetail.message = error.message
errorDetail.type = error.name || error.constructor.name
errorDetail.stack = error.stack
} else if (typeof error === "string") {
errorDetail.message = error
errorDetail.type = "String"
} else if (error === null) {
errorDetail.message = "Null error received"
errorDetail.type = "Null"
} else if (error === undefined) {
errorDetail.message = "Undefined error received"
errorDetail.type = "Undefined"
} else if (typeof error === "object") {
try {
errorDetail.message = error.message || JSON.stringify(error)
errorDetail.type = "Object"
errorDetail.additionalInfo = { ...error }
} catch (e) {
errorDetail.message = "Unserializable error object"
errorDetail.type = "Unserializable"
}
} else {
try {
errorDetail.message = String(error)
errorDetail.type = typeof error
} catch (e) {
errorDetail.message = "Error converting to string"
errorDetail.type = "Unknown"
}
}
// 添加组件信息
if (componentInfo) {
errorDetail.componentInfo = componentInfo
}
// 添加额外信息
if (additionalInfo) {
errorDetail.additionalInfo = {
...errorDetail.additionalInfo,
...additionalInfo,
}
}
return errorDetail
}
/**
*
*/
private saveCrashReport(report: CrashReport): void {
try {
// 生成唯一的崩溃报告文件名
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
const filename = `crash-${timestamp}.json`
const filepath = path.join(this.crashReportDir, filename)
// 写入崩溃报告
fs.writeFileSync(filepath, JSON.stringify(report, null, 2))
logger.info("crash-handler", `Crash report saved: ${filepath}`)
// 清理旧的崩溃报告
this.cleanupOldReports()
} catch (e) {
logger.error("crash-handler", `Failed to save crash report: ${e}`)
}
}
/**
*
*/
private cleanupOldReports(): void {
try {
// 获取所有崩溃报告文件
const files = fs
.readdirSync(this.crashReportDir)
.filter(file => file.startsWith("crash-") && file.endsWith(".json"))
.map(file => ({
name: file,
path: path.join(this.crashReportDir, file),
time: fs.statSync(path.join(this.crashReportDir, file)).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time) // 按时间降序排序
// 删除超出最大数量的旧报告
if (files.length > this.options.maxReports!) {
const filesToDelete = files.slice(this.options.maxReports!)
filesToDelete.forEach(file => {
fs.unlinkSync(file.path)
logger.debug("crash-handler", `Deleted old crash report: ${file.name}`)
})
}
} catch (e) {
logger.error("crash-handler", `Failed to cleanup old reports: ${e}`)
}
}
/**
*
*/
private saveStartupMarker(): void {
try {
const markerPath = path.join(this.crashReportDir, "startup-marker.json")
const marker = {
startTime: this.startTime,
pid: process.pid,
}
fs.writeFileSync(markerPath, JSON.stringify(marker))
} catch (e) {
logger.error("crash-handler", `Failed to save startup marker: ${e}`)
}
}
/**
*
*/
private clearStartupMarker(): void {
try {
const markerPath = path.join(this.crashReportDir, "startup-marker.json")
if (fs.existsSync(markerPath)) {
fs.unlinkSync(markerPath)
}
} catch (e) {
logger.error("crash-handler", `Failed to clear startup marker: ${e}`)
}
}
/**
*
*/
private checkPreviousCrash(): boolean {
try {
const markerPath = path.join(this.crashReportDir, "startup-marker.json")
// 如果存在启动标记,说明上次可能崩溃了
if (fs.existsSync(markerPath)) {
const markerData = JSON.parse(fs.readFileSync(markerPath, "utf8"))
const lastStartTime = markerData.startTime
const lastPid = markerData.pid
logger.warn(
"crash-handler",
`Found previous startup marker. App may have crashed. Last PID: ${lastPid}, Last start time: ${new Date(lastStartTime).toISOString()}`,
)
// 查找最近的崩溃报告
const recentCrash = this.getRecentCrashReport()
// 显示崩溃恢复对话框
if (recentCrash && this.options.showDialog) {
app.whenReady().then(() => {
this.showRecoveryDialog(recentCrash)
})
}
// 清除旧的启动标记
fs.unlinkSync(markerPath)
return true
}
} catch (e) {
logger.error("crash-handler", `Failed to check previous crash: ${e}`)
}
return false
}
/**
*
*/
private getRecentCrashReport(): CrashReport | null {
try {
// 获取所有崩溃报告文件
const files = fs
.readdirSync(this.crashReportDir)
.filter(file => file.startsWith("crash-") && file.endsWith(".json"))
.map(file => ({
name: file,
path: path.join(this.crashReportDir, file),
time: fs.statSync(path.join(this.crashReportDir, file)).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time) // 按时间降序排序
// 读取最近的崩溃报告
if (files.length > 0) {
const recentFile = files[0]
const reportData = fs.readFileSync(recentFile.path, "utf8")
return JSON.parse(reportData) as CrashReport
}
} catch (e) {
logger.error("crash-handler", `Failed to get recent crash report: ${e}`)
}
return null
}
/**
*
*/
private showCrashDialog(error: Error): void {
try {
const options = {
type: "error" as const,
title: "应用崩溃",
message: "应用遇到了一个严重错误,即将关闭",
detail: `错误信息: ${error.message}\n\n堆栈信息: ${error.stack}\n\n崩溃报告已保存,应用将在您点击确定后关闭。`,
buttons: ["确定"],
defaultId: 0,
}
dialog.showMessageBoxSync(options)
// 强制退出应用
setTimeout(() => {
app.exit(1)
}, 1000)
} catch (e) {
logger.error("crash-handler", `Failed to show crash dialog: ${e}`)
app.exit(1)
}
}
/**
*
*/
private showRecoveryDialog(crashReport: CrashReport): void {
try {
const crashTime = new Date(crashReport.timestamp).toLocaleString()
const errorMessage = crashReport.error.message
const errorType = crashReport.error.type
const options = {
type: "warning" as const,
title: "应用恢复",
message: "应用上次异常退出",
detail: `应用在 ${crashTime}${errorType} 错误崩溃: ${errorMessage}\n\n崩溃报告已保存,您可以继续使用应用或联系开发者报告此问题。`,
buttons: ["继续", "查看详情"],
defaultId: 0,
}
const response = dialog.showMessageBoxSync(options)
// 如果用户选择查看详情
if (response === 1) {
// 显示详细的崩溃报告
this.showDetailedCrashInfo(crashReport)
}
} catch (e) {
logger.error("crash-handler", `Failed to show recovery dialog: ${e}`)
}
}
/**
*
*/
private showDetailedCrashInfo(crashReport: CrashReport): void {
try {
const options = {
type: "info" as const,
title: "崩溃详情",
message: "应用崩溃详细信息",
detail: JSON.stringify(crashReport, null, 2),
buttons: ["关闭"],
defaultId: 0,
}
dialog.showMessageBoxSync(options)
} catch (e) {
logger.error("crash-handler", `Failed to show detailed crash info: ${e}`)
}
}
/**
*
*/
public setOptions(options: Partial<CrashHandlerOptions>): void {
this.options = { ...this.options, ...options }
}
/**
*
*/
public getOptions(): CrashHandlerOptions {
return { ...this.options }
}
}
// 创建默认实例
const crashHandler = CrashHandler.getInstance()
export default crashHandler
export { crashHandler }

177
packages/logger/main-error.ts

@ -0,0 +1,177 @@
import { LogLevel, LogLevelName } from "./common"
import logger from "./main"
/**
*
*/
export interface ErrorDetail {
message: string
stack?: string
componentInfo?: string
additionalInfo?: Record<string, any>
timestamp: string
type: string
}
/**
*
*/
export interface ErrorHandlerOptions {
namespace?: string
level?: LogLevel
includeStack?: boolean
includeComponentInfo?: boolean
formatError?: (error: any) => ErrorDetail
}
/**
*
*/
const DEFAULT_OPTIONS: ErrorHandlerOptions = {
namespace: "error",
level: LogLevel.ERROR,
includeStack: true,
includeComponentInfo: true,
}
/**
*
*/
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => {
// 如果已经是ErrorDetail格式,直接返回
if (error && typeof error === "object" && error.type && error.message && error.timestamp) {
return error as ErrorDetail
}
// 基本错误信息
const errorDetail: ErrorDetail = {
message: "",
timestamp: new Date().toISOString(),
type: "Unknown",
}
// 处理不同类型的错误
if (error instanceof Error) {
errorDetail.message = error.message
errorDetail.type = error.name || error.constructor.name
if (options.includeStack) {
errorDetail.stack = error.stack
}
} else if (typeof error === "string") {
errorDetail.message = error
errorDetail.type = "String"
} else if (error === null) {
errorDetail.message = "Null error received"
errorDetail.type = "Null"
} else if (error === undefined) {
errorDetail.message = "Undefined error received"
errorDetail.type = "Undefined"
} else if (typeof error === "object") {
try {
errorDetail.message = error.message || JSON.stringify(error)
errorDetail.type = "Object"
errorDetail.additionalInfo = { ...error }
} catch (e) {
errorDetail.message = "Unserializable error object"
errorDetail.type = "Unserializable"
}
} else {
try {
errorDetail.message = String(error)
errorDetail.type = typeof error
} catch (e) {
errorDetail.message = "Error converting to string"
errorDetail.type = "Unknown"
}
}
return errorDetail
}
/**
*
*/
export class ErrorHandler {
private options: ErrorHandlerOptions
constructor(options?: Partial<ErrorHandlerOptions>) {
this.options = { ...DEFAULT_OPTIONS, ...options }
}
/**
*
*/
public captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void {
const errorDetail = formatError(error, this.options)
// 添加组件信息
if (this.options.includeComponentInfo && componentInfo) {
errorDetail.componentInfo = componentInfo
}
// 添加额外信息
if (additionalInfo) {
errorDetail.additionalInfo = {
...errorDetail.additionalInfo,
...additionalInfo,
}
}
// 使用logger记录错误
const namespace = this.options.namespace || "error"
const level = LogLevelName[this.options.level || LogLevel.ERROR].toLowerCase()
// 构建错误消息
let errorMessage = `${errorDetail.type}: ${errorDetail.message}`
if (errorDetail.componentInfo) {
errorMessage += ` | Component: ${errorDetail.componentInfo}`
}
// 记录错误
logger[level](namespace, errorMessage)
// 如果有堆栈信息,单独记录
if (errorDetail.stack) {
logger[level](namespace, `Stack: ${errorDetail.stack}`)
}
// 如果有额外信息,单独记录
if (errorDetail.additionalInfo) {
try {
const additionalInfoStr = JSON.stringify(errorDetail.additionalInfo, null, 2)
logger[level](namespace, `Additional Info: ${additionalInfoStr}`)
} catch (e) {
logger[level](namespace, "Additional Info: [Unserializable]")
}
}
}
/**
*
*/
public setOptions(options: Partial<ErrorHandlerOptions>): void {
this.options = { ...this.options, ...options }
}
/**
*
*/
public getOptions(): ErrorHandlerOptions {
return { ...this.options }
}
}
// 创建默认实例
const errorHandler = new ErrorHandler()
// 捕获未处理的Promise异常
process.on("unhandledRejection", reason => {
errorHandler.captureError(reason)
})
// 捕获未捕获的异常
process.on("uncaughtException", error => {
errorHandler.captureError(error)
})
export default errorHandler

275
packages/logger/main.ts

@ -0,0 +1,275 @@
import { app, ipcMain } from "electron"
import fs from "fs"
import path from "path"
import setting from "setting/main"
import * as rfs from "rotating-file-stream"
import { LogLevel, LogLevelColor, LogLevelName } from "./common"
// 重置颜色的ANSI代码
const RESET_COLOR = "\x1b[0m"
// 日志配置接口
export interface LoggerOptions {
level?: LogLevel // 日志级别
namespace?: string // 日志命名空间
console?: boolean // 是否输出到控制台
file?: boolean // 是否输出到文件
maxSize?: string // 单个日志文件最大大小
maxFiles?: number // 保留的最大日志文件数量
}
// 默认配置
const DEFAULT_OPTIONS: LoggerOptions = {
level: setting.values("debug"),
namespace: "app",
console: true,
file: true,
maxSize: "10M",
maxFiles: 10,
}
let logDir
const isElectronApp = !!process.versions.electron
if (isElectronApp && app) {
logDir = path.join(app.getPath("logs"))
} else {
// 非Electron环境下使用当前目录下的logs文件夹
logDir = path.join(process.cwd(), "logs")
}
/**
*
*/
export class Logger {
private static instance: Logger
private options: LoggerOptions = DEFAULT_OPTIONS
private logStream: rfs.RotatingFileStream | null = null
private logDir: string = logDir
private currentLogFile: string = ""
private isElectronApp: boolean = !!process.versions.electron
private callInitialize: boolean = false
/**
*
*/
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger()
}
if (Logger.instance.callInitialize) {
return Logger.instance
} else {
// 创建代理对象,确保只有在初始化后才能访问除init之外的方法
const handler = {
get: function (target: any, prop: string) {
if (prop === "init") {
return target[prop]
}
if (!target.callInitialize) {
throw new Error(`Logger未初始化,不能调用${prop}方法,请先调用init()方法`)
}
return target[prop]
},
}
Logger.instance = new Proxy(new Logger(), handler)
}
return Logger.instance
}
/**
*
*/
private constructor() {}
public init(options?: LoggerOptions): void {
this.callInitialize = true
this.options = { ...this.options, ...options }
// 确保日志目录存在
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true })
}
// 初始化日志文件
this.initLogFile()
// 如果在主进程中,设置IPC监听器接收渲染进程的日志
if (this.isElectronApp && process.type === "browser") {
this.setupIPC()
}
}
/**
*
*/
private initLogFile(): void {
if (!this.options.file) return
// 生成日志文件名
const now = new Date()
const timestamp = now.toISOString().replace(/[:.]/g, "-")
this.currentLogFile = `app-logger-${timestamp}.log`
// 创建日志流
this.logStream = rfs.createStream(this.currentLogFile, {
path: this.logDir,
size: this.options.maxSize,
rotate: this.options.maxFiles,
})
}
/**
* IPC通信
*/
private setupIPC(): void {
if (!ipcMain) return
ipcMain.on("logger:log", (_, level: LogLevel, namespace: string, ...messages: any[]) => {
this.logWithLevel(level, namespace, ...messages)
})
// 处理日志级别设置请求
ipcMain.on("logger:setLevel", (_, level: LogLevel) => {
this.setLevel(level)
})
}
/**
*
*/
public close(): void {
if (this.logStream) {
this.logStream.end()
this.logStream.destroy()
this.logStream = null
}
}
/**
*
*/
public setLevel(level: LogLevel): void {
this.options.level = level
}
/**
*
*/
public getLevel(): LogLevel {
return this.options.level ?? LogLevel.INFO
}
/**
*
*/
private logWithLevel(level: LogLevel, namespace: string, ...messages: any[]): void {
// 检查日志级别
if (level < this.getLevel() || level === LogLevel.OFF) return
const timestamp = new Date().toISOString()
const levelName = LogLevelName[level]
const prefix = `[${timestamp}] [${namespace}] [${levelName}]`
// 格式化消息
const formattedMessages = messages.map(msg => {
if (typeof msg === "object") {
try {
return JSON.stringify(msg)
} catch (e) {
return String(msg)
}
}
return String(msg)
})
const message = formattedMessages.join(" ")
// 输出到控制台
if (this.options.console) {
const color = LogLevelColor[level]
console.log(`${color}${prefix} ${message}${RESET_COLOR}`)
}
// 写入日志文件
if (this.options.file && this.logStream) {
this.logStream.write(`${prefix} ${message}\n`)
}
}
/**
*
*/
public trace(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.TRACE, namespace, ...messages)
}
/**
*
*/
public debug(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.DEBUG, namespace, ...messages)
}
/**
*
*/
public info(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.INFO, namespace, ...messages)
}
/**
*
*/
public warn(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.WARN, namespace, ...messages)
}
/**
*
*/
public error(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.ERROR, namespace, ...messages)
}
/**
*
*/
public fatal(namespace: string, ...messages: any[]): void {
this.logWithLevel(LogLevel.FATAL, namespace, ...messages)
}
/**
*
* @param namespace
* @returns
*/
public createNamespace(namespace: string) {
return {
trace: (...messages: any[]) => this.trace(namespace, ...messages),
debug: (...messages: any[]) => this.debug(namespace, ...messages),
info: (...messages: any[]) => this.info(namespace, ...messages),
warn: (...messages: any[]) => this.warn(namespace, ...messages),
error: (...messages: any[]) => this.error(namespace, ...messages),
fatal: (...messages: any[]) => this.fatal(namespace, ...messages),
setLevel: (level: LogLevel) => this.setLevel(level),
getLevel: () => this.getLevel(),
}
}
}
// 默认实例
const logger = Logger.getInstance()
logger.init()
setting.onChange("debug", function(n){
logger.setLevel(n.debug)
})
// 应用退出时关闭日志流
if (process.type === "browser" && app) {
app.on("before-quit", () => {
logger.info("app", "应用关闭")
logger.close()
})
}
export default logger

7
packages/logger/package.json

@ -0,0 +1,7 @@
{
"name": "logger",
"version": "1.0.0",
"keywords": [],
"author": "",
"license": "ISC"
}

195
packages/logger/preload-error.ts

@ -0,0 +1,195 @@
import { contextBridge, ipcRenderer } from "electron"
import { LogLevel, LogLevelName } from "./common"
import logger from "./preload"
/**
*
*/
interface ErrorDetail {
message: string
stack?: string
componentInfo?: string
additionalInfo?: Record<string, any>
timestamp: string
type: string
}
/**
*
*/
interface ErrorHandlerOptions {
namespace?: string
level?: LogLevel
includeStack?: boolean
includeComponentInfo?: boolean
}
/**
*
*/
interface IRendererErrorHandler {
/**
*
*/
captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void
/**
*
*/
setOptions(options: Partial<ErrorHandlerOptions>): void
/**
*
*/
getOptions(): ErrorHandlerOptions
/**
*
*/
installGlobalHandlers(): void
}
/**
*
*/
const DEFAULT_OPTIONS: ErrorHandlerOptions = {
namespace: "error",
level: LogLevel.ERROR,
includeStack: true,
includeComponentInfo: true,
}
/**
*
*/
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => {
// 基本错误信息
const errorDetail: ErrorDetail = {
message: "",
timestamp: new Date().toISOString(),
type: "Unknown",
}
// 处理不同类型的错误
if (error instanceof Error) {
errorDetail.message = error.message
errorDetail.type = error.name || error.constructor.name
if (options.includeStack) {
errorDetail.stack = error.stack
}
} else if (typeof error === "string") {
errorDetail.message = error
errorDetail.type = "String"
} else if (error === null) {
errorDetail.message = "Null error received"
errorDetail.type = "Null"
} else if (error === undefined) {
errorDetail.message = "Undefined error received"
errorDetail.type = "Undefined"
} else if (typeof error === "object") {
try {
errorDetail.message = error.message || JSON.stringify(error)
errorDetail.type = "Object"
errorDetail.additionalInfo = { ...error }
} catch (e) {
errorDetail.message = "Unserializable error object"
errorDetail.type = "Unserializable"
}
} else {
try {
errorDetail.message = String(error)
errorDetail.type = typeof error
} catch (e) {
errorDetail.message = "Error converting to string"
errorDetail.type = "Unknown"
}
}
return errorDetail
}
/**
*
*/
const createRendererErrorHandler = (): IRendererErrorHandler => {
// 当前错误处理选项
let options: ErrorHandlerOptions = { ...DEFAULT_OPTIONS }
/**
*
*/
const handleError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => {
// 如果已经是ErrorDetail格式,直接使用
let errorDetail: ErrorDetail
if (error && typeof error === "object" && error.type && error.message && error.timestamp) {
errorDetail = error as ErrorDetail
} else {
// 否则格式化错误
errorDetail = formatError(error, options)
}
// 添加组件信息
if (options.includeComponentInfo && componentInfo) {
errorDetail.componentInfo = componentInfo
}
// 使用logger记录错误
const namespace = options.namespace || "error"
const level = LogLevelName[options.level || LogLevel.ERROR].toLowerCase()
// 添加额外信息
if (additionalInfo) {
errorDetail.additionalInfo = {
...errorDetail.additionalInfo,
...additionalInfo,
}
}
// 记录完整的错误信息
logger[level](namespace, JSON.stringify(errorDetail))
// 同时在控制台输出错误信息
logger[level](namespace, `${errorDetail.type}: ${errorDetail.message}`)
if (errorDetail.stack) {
logger[level](namespace, `Stack: ${errorDetail.stack}`)
}
// 如果有额外信息,单独记录
if (errorDetail.additionalInfo) {
try {
const additionalInfoStr = JSON.stringify(errorDetail.additionalInfo, null, 2)
logger[level](namespace, `Additional Info: ${additionalInfoStr}`)
} catch (e) {
logger[level](namespace, "Additional Info: [Unserializable]")
}
}
}
/**
*
* renderer-error.ts负责
*/
const installGlobalHandlers = () => {
// 不再在preload层安装全局错误处理器
// 仅记录日志表明该方法被调用
logger.info("[ErrorHandler] Global error handlers should be installed in renderer process")
}
return {
captureError: handleError,
setOptions: (newOptions: Partial<ErrorHandlerOptions>) => {
options = { ...options, ...newOptions }
// 同步选项到主进程
ipcRenderer.send("logger:errorOptions", options)
},
getOptions: () => ({ ...options }),
installGlobalHandlers,
}
}
const errorHandler = createRendererErrorHandler()
// 暴露错误处理器到渲染进程全局
contextBridge.exposeInMainWorld("preloadErrorHandler", errorHandler)
// 导出类型定义,方便在渲染进程中使用
export type { IRendererErrorHandler, ErrorDetail, ErrorHandlerOptions }

153
packages/logger/preload.ts

@ -0,0 +1,153 @@
import { contextBridge, ipcRenderer } from "electron"
import { LogLevel } from "./common"
/**
*
*/
interface IRendererLogger {
trace(namespace: string, ...messages: any[]): void
debug(namespace: string, ...messages: any[]): void
info(namespace: string, ...messages: any[]): void
warn(namespace: string, ...messages: any[]): void
error(namespace: string, ...messages: any[]): void
fatal(namespace: string, ...messages: any[]): void
setLevel(level: LogLevel): void
createNamespace(namespace: string): INamespacedLogger
}
/**
*
*/
interface INamespacedLogger {
trace(...messages: any[]): void
debug(...messages: any[]): void
info(...messages: any[]): void
warn(...messages: any[]): void
error(...messages: any[]): void
fatal(...messages: any[]): void
setLevel(level: LogLevel): void
}
// 日志级别名称映射
const LogLevelName: Record<LogLevel, string> = {
[LogLevel.TRACE]: "TRACE",
[LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO",
[LogLevel.WARN]: "WARN",
[LogLevel.ERROR]: "ERROR",
[LogLevel.FATAL]: "FATAL",
[LogLevel.OFF]: "OFF",
}
// 日志颜色映射(控制台输出用)
const LogLevelColor: Record<LogLevel, string> = {
[LogLevel.TRACE]: "\x1b[90m", // 灰色
[LogLevel.DEBUG]: "\x1b[36m", // 青色
[LogLevel.INFO]: "\x1b[32m", // 绿色
[LogLevel.WARN]: "\x1b[33m", // 黄色
[LogLevel.ERROR]: "\x1b[31m", // 红色
[LogLevel.FATAL]: "\x1b[35m", // 紫色
[LogLevel.OFF]: "", // 无色
}
// 重置颜色的ANSI代码
const RESET_COLOR = "\x1b[0m"
/**
*
*/
const createRendererLogger = (): IRendererLogger => {
// 当前日志级别
let currentLevel: LogLevel = LogLevel.INFO
// 格式化消息
const formatMessages = (messages: any[]): string => {
return messages
.map(msg => {
if (typeof msg === "object") {
try {
return JSON.stringify(msg)
} catch (e) {
return String(msg)
}
}
return String(msg)
})
.join(" ")
}
// 本地打印日志
const printLog = (level: LogLevel, namespace: string, ...messages: any[]): void => {
// 检查日志级别
if (level < currentLevel || level === LogLevel.OFF) return
const timestamp = new Date().toISOString()
const levelName = LogLevelName[level]
const prefix = `[${timestamp}] [${namespace}] [${levelName}]`
const message = formatMessages(messages)
// 输出到控制台
const color = LogLevelColor[level]
console.log(`${color}${prefix} ${message}${RESET_COLOR}`)
}
// 通过IPC发送日志到主进程
const sendLog = (level: LogLevel, namespace: string, ...messages: any[]) => {
// 本地打印
printLog(level, namespace, ...messages)
// 发送到主进程
ipcRenderer.send("logger:log", level, namespace, ...messages)
}
return {
trace(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.TRACE, namespace, ...messages)
},
debug(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.DEBUG, namespace, ...messages)
},
info(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.INFO, namespace, ...messages)
},
warn(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.WARN, namespace, ...messages)
},
error(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.ERROR, namespace, ...messages)
},
fatal(namespace: string, ...messages: any[]): void {
sendLog(LogLevel.FATAL, namespace, ...messages)
},
setLevel(level: LogLevel): void {
// 更新本地日志级别
currentLevel = level
// 设置日志级别(可选,如果需要在渲染进程中动态调整日志级别)
ipcRenderer.send("logger:setLevel", level)
},
createNamespace(namespace: string): INamespacedLogger {
return {
trace: (...messages: any[]) => sendLog(LogLevel.TRACE, namespace, ...messages),
debug: (...messages: any[]) => sendLog(LogLevel.DEBUG, namespace, ...messages),
info: (...messages: any[]) => sendLog(LogLevel.INFO, namespace, ...messages),
warn: (...messages: any[]) => sendLog(LogLevel.WARN, namespace, ...messages),
error: (...messages: any[]) => sendLog(LogLevel.ERROR, namespace, ...messages),
fatal: (...messages: any[]) => sendLog(LogLevel.FATAL, namespace, ...messages),
setLevel: (level: LogLevel) => {
currentLevel = level
ipcRenderer.send("logger:setLevel", level)
},
}
},
}
}
const logger = createRendererLogger()
// 暴露logger对象到渲染进程全局
contextBridge.exposeInMainWorld("logger", logger)
export { logger }
export default logger
// 导出类型定义,方便在渲染进程中使用
export type { IRendererLogger }

243
packages/logger/renderer-error.ts

@ -0,0 +1,243 @@
import { LogLevel } from "./common"
/**
*
*/
interface ErrorDetail {
message: string
stack?: string
componentInfo?: string
additionalInfo?: Record<string, any>
timestamp: string
type: string
}
/**
*
*/
interface ErrorHandlerOptions {
namespace?: string
level?: LogLevel
includeStack?: boolean
includeComponentInfo?: boolean
}
/**
*
*/
export interface IRendererErrorHandler {
/**
*
*/
captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void
/**
*
*/
setOptions(options: Partial<ErrorHandlerOptions>): void
/**
*
*/
getOptions(): ErrorHandlerOptions
/**
*
*/
installGlobalHandlers(): void
}
/**
*
*/
const DEFAULT_OPTIONS: ErrorHandlerOptions = {
namespace: "error",
level: LogLevel.ERROR,
includeStack: true,
includeComponentInfo: true,
}
/**
*
*/
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => {
// 基本错误信息
const errorDetail: ErrorDetail = {
message: "",
timestamp: new Date().toISOString(),
type: "Unknown",
}
console.log(error)
// 处理不同类型的错误
if (error instanceof Error) {
errorDetail.message = error.message
errorDetail.type = error.name || error.constructor.name
if (options.includeStack) {
errorDetail.stack = error.stack
}
} else if (typeof error === "string") {
errorDetail.message = error
errorDetail.type = "String"
} else if (error === null) {
errorDetail.message = "Null error received"
errorDetail.type = "Null"
} else if (error === undefined) {
errorDetail.message = "Undefined error received"
errorDetail.type = "Undefined"
} else if (typeof error === "object") {
try {
errorDetail.message = error.message || JSON.stringify(error)
errorDetail.type = "Object"
errorDetail.additionalInfo = { ...error }
} catch (e) {
errorDetail.message = "Unserializable error object"
errorDetail.type = "Unserializable"
}
} else {
try {
errorDetail.message = String(error)
errorDetail.type = typeof error
} catch (e) {
errorDetail.message = "Error converting to string"
errorDetail.type = "Unknown"
}
}
return errorDetail
}
// @ts-ignore
const preloadErrorHandler = window.preloadErrorHandler
/**
*
*/
export const createRendererErrorHandler = (): IRendererErrorHandler => {
// 当前错误处理选项
let options: ErrorHandlerOptions = { ...DEFAULT_OPTIONS }
/**
*
*/
const processError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>): ErrorDetail => {
const errorDetail = formatError(error, options)
// 添加组件信息
if (options.includeComponentInfo && componentInfo) {
errorDetail.componentInfo = componentInfo
}
// 添加额外信息
if (additionalInfo) {
errorDetail.additionalInfo = {
...errorDetail.additionalInfo,
...additionalInfo,
}
}
return errorDetail
}
/**
* preload层
*/
const sendError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => {
// 处理并序列化错误
const errorDetail = processError(error, componentInfo, additionalInfo)
// 调用window.errorHandler.captureError发送错误
// 这里假设preload层已经暴露了errorHandler对象
if (preloadErrorHandler && typeof preloadErrorHandler.captureError === "function") {
preloadErrorHandler.captureError(errorDetail)
} else {
// 如果errorHandler不可用,则降级到控制台输出
console.error("[ErrorHandler]", errorDetail)
}
}
/**
*
*/
const installGlobalHandlers = () => {
// 捕获未处理的异常
window.addEventListener("error", event => {
event.preventDefault()
sendError(event.error || event.message, "window.onerror", {
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
})
return true
})
// 捕获未处理的Promise拒绝
window.addEventListener("unhandledrejection", event => {
event.preventDefault()
sendError(event.reason, "unhandledrejection", {
promise: "[Promise]", // 不能直接序列化Promise对象
})
return true
})
// 捕获资源加载错误
document.addEventListener(
"error",
event => {
// 只处理资源加载错误
if (event.target && (event.target as HTMLElement).tagName) {
const target = event.target as HTMLElement
sendError(`Resource load failed: ${(target as any).src || (target as any).href}`, "resource.error", {
tagName: target.tagName,
src: (target as any).src,
href: (target as any).href,
})
}
},
true,
) // 使用捕获阶段
console.info("[ErrorHandler] Global error handlers installed")
}
return {
captureError: sendError,
setOptions: (newOptions: Partial<ErrorHandlerOptions>) => {
options = { ...options, ...newOptions }
// 同步选项到preload层
if (preloadErrorHandler && typeof preloadErrorHandler.setOptions === "function") {
preloadErrorHandler.setOptions(options)
}
},
getOptions: () => ({ ...options }),
installGlobalHandlers,
}
}
// 导出类型定义,方便在渲染进程中使用
export type { ErrorDetail, ErrorHandlerOptions }
// 创建渲染进程错误处理器
const errorHandler = createRendererErrorHandler()
// 安装全局错误处理器
errorHandler.installGlobalHandlers()
window.errorHandler = errorHandler
/**
* 使
*
* // 捕获特定错误
* try {
* // 可能出错的代码
* } catch (error) {
* errorHandler.captureError(error, 'ComponentName', { additionalInfo: 'value' })
* }
*
* // 设置错误处理选项
* errorHandler.setOptions({
* namespace: 'custom-error',
* includeComponentInfo: true
* })
*/

230
packages/setting/main.ts

@ -0,0 +1,230 @@
import fs from "fs-extra"
import { app } from "electron"
import path from "path"
import { cloneDeep } from "lodash"
import Config from "config"
import type { IDefaultConfig } from "config"
import _debug from "debug"
const debug = _debug("app:setting")
type IConfig = IDefaultConfig
type IOnFunc = (n: IConfig, c: IConfig, keys?: (keyof IConfig)[]) => void
type IT = (keyof IConfig)[] | keyof IConfig | "_"
let storagePath = path.join(app.getPath("documents"), Config.app_title)
const storagePathDev = path.join(app.getPath("documents"), Config.app_title + "-dev")
if (process.env.NODE_ENV === "development") {
storagePath = storagePathDev
}
const _tempConfig = cloneDeep(Config.default_config as IConfig)
Object.keys(_tempConfig).forEach(key => {
if (typeof _tempConfig[key] === "string" && _tempConfig[key].includes("$storagePath$")) {
_tempConfig[key] = _tempConfig[key].replace(/\$storagePath\$/g, storagePath)
if (_tempConfig[key] && path.isAbsolute(_tempConfig[key])) {
_tempConfig[key] = path.normalize(_tempConfig[key])
}
}
})
function isPath(str) {
// 使用正则表达式检查字符串是否以斜杠或盘符开头
return /^(?:\/|[a-zA-Z]:\\)/.test(str)
}
function init(config: IConfig) {
// 在配置初始化后执行
Object.keys(config).forEach(key => {
if (config[key] && isPath(config[key]) && path.isAbsolute(config[key])) {
fs.ensureDirSync(config[key])
}
})
// 在配置初始化后执行
// fs.ensureDirSync(config["snippet.storagePath"])
// fs.ensureDirSync(config["bookmark.storagePath"])
}
// 判断是否是空文件夹
function isEmptyDir(fPath: string) {
const pa = fs.readdirSync(fPath)
if (pa.length === 0) {
return true
} else {
return false
}
}
class SettingClass {
constructor() {
debug(`Setting inited`)
this.init()
}
#cb: [IT, IOnFunc][] = []
onChange(fn: IOnFunc, that?: any)
onChange(key: IT, fn: IOnFunc, that?: any)
onChange(fnOrType: IT | IOnFunc, fnOrThat: IOnFunc | any = null, that: any = null) {
if (typeof fnOrType === "function") {
this.#cb.push(["_", fnOrType.bind(fnOrThat)])
} else {
this.#cb.push([fnOrType, fnOrThat.bind(that)])
}
}
#runCB(n: IConfig, c: IConfig, keys: (keyof IConfig)[]) {
for (let i = 0; i < this.#cb.length; i++) {
const temp = this.#cb[i]
const k = temp[0]
const fn = temp[1]
if (k === "_") {
fn(n, c, keys)
}
if (typeof k === "string" && keys.includes(k as keyof IConfig)) {
fn(n, c)
}
if (Array.isArray(k) && k.filter(v => keys.indexOf(v) !== -1).length) {
fn(n, c)
}
}
}
#pathFile: string =
process.env.NODE_ENV === "development"
? path.resolve(app.getPath("userData"), "./config_path-dev")
: path.resolve(app.getPath("userData"), "./config_path")
#config: IConfig = cloneDeep(_tempConfig)
#configPath(storagePath?: string): string {
return path.join(storagePath || this.#config.storagePath, "./config.json")
}
/**
*
* @param confingPath
*/
#syncVar(confingPath?: string) {
const configFile = this.#configPath(confingPath)
if (!fs.pathExistsSync(configFile)) {
fs.ensureFileSync(configFile)
fs.writeJSONSync(configFile, {})
}
const config = fs.readJSONSync(configFile) as IConfig
confingPath && (config.storagePath = confingPath)
// 优先取本地的值
for (const key in config) {
// if (Object.prototype.hasOwnProperty.call(this.#config, key)) {
// this.#config[key] = config[key] || this.#config[key]
// }
// 删除配置时本地的配置不会改变,想一下哪种方式更好
this.#config[key] = config[key] ?? this.#config[key]
}
}
init() {
debug(`位置:${this.#pathFile}`)
if (fs.pathExistsSync(this.#pathFile)) {
const confingPath = fs.readFileSync(this.#pathFile, { encoding: "utf8" })
if (confingPath && fs.pathExistsSync(this.#configPath(confingPath))) {
this.#syncVar(confingPath)
// 防止增加了配置本地却没变的情况
this.#sync(confingPath)
} else {
this.#syncVar(confingPath)
this.#sync(confingPath)
}
} else {
this.#syncVar()
this.#sync()
}
init.call(this, this.#config)
}
config() {
return this.#config
}
#sync(c?: string) {
const config = cloneDeep(this.#config)
delete config.storagePath
const p = this.#configPath(c)
fs.ensureFileSync(p)
fs.writeJSONSync(this.#configPath(c), config)
}
#change(p: string) {
const storagePath = this.#config.storagePath
if (fs.existsSync(storagePath) && !fs.existsSync(p)) {
fs.moveSync(storagePath, p)
}
if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) {
fs.moveSync(storagePath, p, { overwrite: true })
}
fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" })
}
reset(key: keyof IConfig) {
this.set(key, cloneDeep(_tempConfig[key]))
}
set(key: keyof IConfig | Partial<IConfig>, value?: any) {
const oldMainConfig = Object.assign({}, this.#config)
let isChange = false
const changeKeys: (keyof IConfig)[] = []
const canChangeStorage = (targetPath: string) => {
if (fs.existsSync(oldMainConfig.storagePath) && fs.existsSync(targetPath) && !isEmptyDir(targetPath)) {
if (fs.existsSync(path.join(targetPath, "./config.json"))) {
return true
}
return false
}
return true
}
if (typeof key === "string") {
if (value != undefined && value !== this.#config[key]) {
if (key === "storagePath") {
if (!canChangeStorage(value)) {
throw "无法改变存储地址"
return
}
this.#change(value)
changeKeys.push("storagePath")
this.#config["storagePath"] = value
} else {
changeKeys.push(key)
this.#config[key as string] = value
}
isChange = true
}
} else {
if (key["storagePath"] !== undefined && key["storagePath"] !== this.#config["storagePath"]) {
if (!canChangeStorage(key["storagePath"])) {
throw "无法改变存储地址"
return
}
this.#change(key["storagePath"])
this.#config["storagePath"] = key["storagePath"]
changeKeys.push("storagePath")
isChange = true
}
for (const _ in key) {
if (Object.prototype.hasOwnProperty.call(key, _)) {
const v = key[_]
if (v != undefined && _ !== "storagePath" && v !== this.#config[_]) {
this.#config[_] = v
changeKeys.push(_ as keyof IConfig)
isChange = true
}
}
}
}
if (isChange) {
this.#sync()
this.#runCB(this.#config, oldMainConfig, changeKeys)
}
}
values<T extends keyof IConfig>(key: T): IConfig[T] {
return this.#config[key]
}
}
const Setting = new SettingClass()
export default Setting
export { Setting }

7
packages/setting/package.json

@ -0,0 +1,7 @@
{
"name": "setting",
"version": "1.0.0",
"keywords": [],
"author": "",
"license": "ISC"
}

391
pnpm-lock.yaml

@ -14,15 +14,6 @@ importers:
'@electron-toolkit/utils':
specifier: ^3.0.0
version: 3.0.0(electron@31.7.7)
'@types/debug':
specifier: ^4.1.12
version: 4.1.12
'@unocss/reset':
specifier: ^0.64.1
version: 0.64.1
'@vueuse/core':
specifier: ^12.7.0
version: 12.7.0(typescript@5.7.3)
electron-updater:
specifier: ^6.3.9
version: 6.3.9
@ -35,24 +26,6 @@ importers:
reflect-metadata:
specifier: ^0.2.2
version: 0.2.2
sass:
specifier: ^1.85.0
version: 1.85.0
unplugin-auto-import:
specifier: ^19.1.0
version: 19.1.0(@vueuse/core@12.7.0(typescript@5.7.3))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.4.0(@babel/parser@7.26.9)(vue@3.5.13(typescript@5.7.3))
unplugin-vue-macros:
specifier: ^2.14.2
version: 2.14.2(@vueuse/core@12.7.0(typescript@5.7.3))(esbuild@0.23.1)(rollup@4.26.0)(typescript@5.7.3)(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue-tsc@2.1.10(typescript@5.7.3))(vue@3.5.13(typescript@5.7.3))
unplugin-vue-router:
specifier: ^0.11.2
version: 0.11.2(rollup@4.26.0)(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
vue-router:
specifier: ^4.5.0
version: 4.5.0(vue@3.5.13(typescript@5.7.3))
devDependencies:
'@electron-toolkit/eslint-config':
specifier: ^1.0.2
@ -63,15 +36,24 @@ 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
'@types/debug':
specifier: ^4.1.12
version: 4.1.12
'@types/node':
specifier: ^20.17.19
version: 20.17.19
'@unocss/preset-rem-to-px':
specifier: ^0.64.1
version: 0.64.1
'@unocss/reset':
specifier: ^0.64.1
version: 0.64.1
'@vitejs/plugin-vue':
specifier: ^5.2.1
version: 5.2.1(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue@3.5.13(typescript@5.7.3))
@ -84,6 +66,9 @@ importers:
'@vue/eslint-config-typescript':
specifier: ^13.0.0
version: 13.0.0(eslint-plugin-vue@9.32.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.7.3)
'@vueuse/core':
specifier: ^12.7.0
version: 12.7.0(typescript@5.7.3)
debug:
specifier: ^4.4.0
version: 4.4.0
@ -102,12 +87,33 @@ importers:
eslint-plugin-vue:
specifier: ^9.32.0
version: 9.32.0(eslint@8.57.1)
extract-zip:
specifier: ^2.0.1
version: 2.0.1
locales:
specifier: workspace:*
version: link:packages/locales
lodash-es:
specifier: ^4.17.21
version: 4.17.21
logger:
specifier: workspace:^
version: link:packages/logger
monaco-editor:
specifier: ^0.52.2
version: 0.52.2
prettier:
specifier: ^3.5.1
version: 3.5.1
rotating-file-stream:
specifier: ^3.2.6
version: 3.2.6
sass:
specifier: ^1.85.0
version: 1.85.0
setting:
specifier: workspace:^
version: link:packages/setting
simplebar-vue:
specifier: ^2.4.0
version: 2.4.0(vue@3.5.13(typescript@5.7.3))
@ -117,19 +123,49 @@ importers:
unocss:
specifier: ^0.64.1
version: 0.64.1(postcss@8.4.49)(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))
unplugin-auto-import:
specifier: ^19.1.0
version: 19.1.0(@vueuse/core@12.7.0(typescript@5.7.3))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.4.0(@babel/parser@7.26.9)(vue@3.5.13(typescript@5.7.3))
unplugin-vue-macros:
specifier: ^2.14.2
version: 2.14.2(@vueuse/core@12.7.0(typescript@5.7.3))(esbuild@0.23.1)(rollup@4.26.0)(typescript@5.7.3)(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue-tsc@2.1.10(typescript@5.7.3))(vue@3.5.13(typescript@5.7.3))
unplugin-vue-router:
specifier: ^0.11.2
version: 0.11.2(rollup@4.26.0)(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
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))
vscode-icons-js:
specifier: ^11.6.1
version: 11.6.1
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-router:
specifier: ^4.5.0
version: 4.5.0(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/logger: {}
packages/setting: {}
packages:
7zip-bin@5.2.0:
@ -675,6 +711,73 @@ 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.1.1':
resolution: {integrity: sha512-4iEsUZ3aF7jXY19CJFN5VP+pPyLITD9FVsjB13z9TU1UxaZLlFsmNhvRxlPDSOfHAP5RpNF2QKKdZ3DHVf4Yzw==}
engines: {node: '>= 16'}
'@intlify/message-compiler@12.0.0-alpha.2':
resolution: {integrity: sha512-PD9C+oQbb7BF52hec0+vLnScaFkvnfX+R7zSbODYuRo/E2niAtGmHd0wPvEMsDhf9Z9b8f/qyDsVeZnD/ya9Ug==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.1':
resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.3':
resolution: {integrity: sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==}
engines: {node: '>= 16'}
'@intlify/shared@12.0.0-alpha.2':
resolution: {integrity: sha512-P2DULVX9nz3y8zKNqLw9Es1aAgQ1JGC+kgpx5q7yLmrnAKkPR5MybQWoEhxanefNJgUY5ehsgo+GKif59SrncA==}
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==}
@ -755,25 +858,21 @@ packages:
resolution: {integrity: sha512-eEwxY+0Cf76HnQwr1+Qy48qwf4dAebTHaKhzEgxLqLK6szbglnK6SThjY95YHrYWwsH1GujWiFoX51jwZNYfSw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-arm64-musl@3.0.3':
resolution: {integrity: sha512-LdxbLv8qVkzro4/ZoP9MuytIL6NOVsbhoZ5Wl1KXOa/2DSxBiksrAPMSChCTyeLy6P3ebSHxQSb52ku18t1LBA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-linux-x64-gnu@3.0.3':
resolution: {integrity: sha512-bN8elR9AV/DZZPdcteOWWElkz8KyxLtOvmfVl7Dnehcs6f9e+fWYKyqiKvva1jsxG4znGKCPT1gfMhpYW8QuKg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-x64-musl@3.0.3':
resolution: {integrity: sha512-Zy1U49BjriwbAds2ho6CGjZIk2KVn0+lrc/G5bvhQg7UJYxEkAueMGBuA5rULIhx9xVtIPsT9Q+J5Xhb4ffVNw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-wasm32-wasi@3.0.3':
resolution: {integrity: sha512-7rteQnn7i5f9nkFZs1VRdBqFhvOx3zWavyKkWjXYVxc9vsSLTg0moh2MRZw5dw5m/bEi1u/p3YAKJ9gdHyBhNQ==}
@ -819,42 +918,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.0':
resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.0':
resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.0':
resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.0':
resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.0':
resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.0':
resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
@ -941,55 +1034,46 @@ packages:
resolution: {integrity: sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.26.0':
resolution: {integrity: sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.26.0':
resolution: {integrity: sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.26.0':
resolution: {integrity: sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.26.0':
resolution: {integrity: sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.26.0':
resolution: {integrity: sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.26.0':
resolution: {integrity: sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.26.0':
resolution: {integrity: sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.26.0':
resolution: {integrity: sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.26.0':
resolution: {integrity: sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==}
@ -1039,6 +1123,9 @@ packages:
'@types/http-cache-semantics@4.0.4':
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
'@types/jasmine@3.10.18':
resolution: {integrity: sha512-jOk52a1Kz+1oU5fNWwAcNe64/GsE7r/Q6ronwDox0D3ETo/cr4ICMQyeXrj7G6FPW1n8YjRoAZA2F0XBr6GicQ==}
'@types/keyv@3.1.4':
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
@ -1088,6 +1175,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 +1193,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 +1206,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 +1222,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 +2081,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 +2120,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 +2134,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 +2529,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 +2572,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 +2711,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 +3168,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 +3343,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:
@ -3246,6 +3386,9 @@ packages:
terser:
optional: true
vscode-icons-js@11.6.1:
resolution: {integrity: sha512-rht18IFYv117UlqBn6o9j258SOtwhDBmtVrGwdoLPpSj6Z5LKQIzarQDd/tCRWneU68KEX25+nsh48tAoknKNw==}
vscode-uri@3.0.8:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
@ -3271,6 +3414,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 +3480,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 +4021,77 @@ 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': 12.0.0-alpha.2
'@intlify/shared': 12.0.0-alpha.2
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.1.1':
dependencies:
'@intlify/shared': 11.1.1
source-map-js: 1.2.1
'@intlify/message-compiler@12.0.0-alpha.2':
dependencies:
'@intlify/shared': 12.0.0-alpha.2
source-map-js: 1.2.1
'@intlify/shared@11.1.1': {}
'@intlify/shared@11.1.3': {}
'@intlify/shared@12.0.0-alpha.2': {}
'@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.3
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.3)(@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.3)(@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.3
'@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)':
@ -4146,6 +4370,8 @@ snapshots:
'@types/http-cache-semantics@4.0.4': {}
'@types/jasmine@3.10.18': {}
'@types/keyv@3.1.4':
dependencies:
'@types/node': 20.17.19
@ -4212,6 +4438,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 +4457,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 +4474,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 +4504,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 +5703,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 +5745,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 +5796,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 +6218,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 +6268,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 +6387,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 +6861,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 +7115,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
@ -6856,6 +7139,10 @@ snapshots:
fsevents: 2.3.3
sass: 1.85.0
vscode-icons-js@11.6.1:
dependencies:
'@types/jasmine': 3.10.18
vscode-uri@3.0.8: {}
vue-demi@0.13.11(vue@3.5.13(typescript@5.7.3)):
@ -6879,6 +7166,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 +7227,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: {}

2
pnpm-workspace.yaml

@ -1,2 +1,2 @@
packages:
- "packages/*"
- "packages/*"

17
resources/fuck.html

@ -1,11 +1,12 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
前往 <a href="https://baidu.com" target="_blank">百度</a>
</body>
</head>
<body>
前往
<a href="https://baidu.com" target="_blank">百度</a>
</body>
</html>

17
src/common/_ioc.main.ts

@ -0,0 +1,17 @@
import { Container, ContainerModule } from "inversify"
import UpdateCommand from "common/event/Update/main/command"
import PlatFormCommand from "common/event/PlatForm/main/command"
import TabsCommand from "common/event/Tabs/main/command"
const modules = new ContainerModule(bind => {
bind("TabsCommand").to(TabsCommand).inSingletonScope()
bind("PlatFormCommand").to(PlatFormCommand).inSingletonScope()
bind("UpdateCommand").to(UpdateCommand).inSingletonScope()
})
async function destroyAllCommand(ioc: Container) {
await ioc.unloadAsync(modules)
}
export { modules, destroyAllCommand }
export default modules

5
src/common/event/PlatForm/hook.ts

@ -0,0 +1,5 @@
import { PlatForm } from "."
export function usePlatForm() {
return PlatForm.getInstance<PlatForm>()
}

48
src/common/event/PlatForm/index.ts

@ -0,0 +1,48 @@
import { _Base } from "common/lib/_Base"
import { ApiFactory } from "common/lib/abstract"
import { LogLevel } from "packages/logger/common"
class PlatForm extends _Base {
constructor() {
super()
}
private get api() {
return ApiFactory.getApiClient()
}
async logSetLevel(level: LogLevel) {
return this.api.call("PlatFormCommand.logSetLevel", level)
}
async logGetLevel() {
return this.api.call("PlatFormCommand.logGetLevel")
}
async showAbout() {
// return this.api.call("BasicService.showAbout")
return this.api.call("PlatFormCommand.showAbout")
}
async crash() {
return this.api.call("PlatFormCommand.crash")
}
async isFullScreen() {
return this.api.call("PlatFormCommand.isFullscreen")
}
async toggleFullScreen() {
return this.api.call("PlatFormCommand.fullscreen")
}
async reload() {
return this.api.call("PlatFormCommand.reload")
}
async toggleDevTools() {
return this.api.call("PlatFormCommand.toggleDevTools")
}
}
export { PlatForm }

120
src/common/event/PlatForm/main/command.ts

@ -0,0 +1,120 @@
import { app, dialog, nativeTheme, TitleBarOverlayOptions } from "electron"
import { inject } from "inversify"
import errorHandler from "logger/main-error"
import Tabs from "main/modules/tabs"
import WindowManager from "main/modules/window-manager"
import { getFileUrl } from "main/utils"
import icon from "@res/icon.png?asset"
import setting from "setting/main"
import { LogLevel } from "logger/common"
export default class PlatFormCommand {
constructor(
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Tabs) private _Tabs: Tabs,
) {}
setTheme(theme: typeof nativeTheme.themeSource) {
nativeTheme.themeSource = theme
}
logSetLevel(level: LogLevel) {
return setting.set("debug", level)
}
logGetLevel() {
return setting.values("debug")
}
setTitlBar(options: TitleBarOverlayOptions) {
const mainWindow = this._WindowManager.getMainWindow()
if (mainWindow) {
mainWindow.setTitleBarOverlay(options)
}
}
showAbout() {
this._WindowManager.createWindow("about", {
url: getFileUrl("about.html"),
overideWindowOpts: true,
confrimWindowClose: false,
type: "info",
windowOpts: {
width: 600,
height: 400,
minimizable: false,
darkTheme: true,
modal: true,
show: false,
resizable: false,
icon: icon,
webPreferences: {
devTools: false,
sandbox: false,
nodeIntegration: false,
contextIsolation: true,
},
},
})
}
toggleDevTools() {
const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) {
// @ts-ignore ...
focusedWindow.toggleDevTools()
}
}
fullscreen() {
const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) {
const isFullScreen = focusedWindow!.isFullScreen()
focusedWindow!.setFullScreen(!isFullScreen)
}
}
crash() {
errorHandler.captureError(new Error("手动触发的崩溃"))
process.crash()
}
isFullscreen() {
const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) {
return focusedWindow!.isFullScreen()
}
return false
}
relunch() {
app.relaunch()
app.exit()
}
reload() {
const focusedWindow = this._WindowManager.getFocusWindow()
// 重载之后, 刷新并关闭所有的次要窗体
if (this._WindowManager.length() > 1 && focusedWindow && focusedWindow.$$opts!.name === this._WindowManager.mainInfo.name) {
const choice = dialog.showMessageBoxSync(focusedWindow, {
type: "question",
buttons: ["取消", "是的,继续", "不,算了"],
title: "警告",
defaultId: 2,
cancelId: 0,
message: "警告",
detail: "重载主窗口将关闭所有子窗口,是否继续",
})
if (choice == 1) {
this._WindowManager.getWndows().forEach(win => {
if (win.$$opts!.name !== this._WindowManager.mainInfo.name) {
win.close()
}
})
} else {
return
}
}
this._Tabs.closeAll()
focusedWindow!.reload()
}
}

54
src/common/event/Tabs/index.ts

@ -0,0 +1,54 @@
import { _Base } from "../../lib/_Base"
export class Tabs extends _Base {
constructor() {
super()
}
private isListen: boolean = false
private execUpdate = (...args) => {
this.#fnList.forEach(v => v(...args))
}
#fnList: ((...args) => void)[] = []
listenUpdate(cb: (...args) => void) {
if (!this.isListen) {
api.on("main:TabsCommand.update", this.execUpdate)
this.isListen = true
}
this.#fnList.push(cb)
}
unListenUpdate(fn: (...args) => void) {
this.#fnList = this.#fnList.filter(v => {
return v !== fn
})
if (!this.#fnList.length) {
api.off("main:TabsCommand.update", this.execUpdate)
this.isListen = false
}
}
bindPosition(data) {
api.call("TabsCommand.bindElement", data)
}
closeAll() {
api.call("TabsCommand.closeAll")
}
sync() {
api.call("TabsCommand.sync")
}
unListenerAll() {
this.#fnList = []
api.offAll("main:TabsCommand.update")
}
async getAllTabs() {
const res = await api.call("TabsCommand.getAllTabs")
return res
}
}

65
src/common/event/Tabs/main/command.ts

@ -0,0 +1,65 @@
import { inject } from "inversify"
import Tabs from "main/modules/tabs"
import WindowManager from "main/modules/window-manager"
import { broadcast } from "main/utils"
class TabsCommand {
constructor(
@inject(Tabs) private _Tabs: Tabs,
@inject(WindowManager) private _WindowManager: WindowManager,
) {
this._Tabs.events.on("update", this.listenerTabActive)
}
listenerTabActive = () => {
broadcast("main:TabsCommand.update", this.getAllTabs())
}
bindElement(rect) {
this._Tabs.updateRect(rect)
}
reload() {
this._WindowManager.getMainWindow()?.reload()
}
sync() {
this.listenerTabActive()
if (!this.getAllTabs().length) {
this.add("about:blank")
}
}
add(url) {
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!)
}
nagivate(index: number, url: string) {
this._Tabs.navigate(+index, url)
}
closeAll() {
this._Tabs.closeAll()
}
setActive(index) {
this._Tabs.changeActive(index)
}
closeTab(e) {
this._Tabs.remove(e.body.active)
}
getAllTabs() {
return this._Tabs._tabs.map(v => ({
url: v.url,
showUrl: v.showUrl,
title: v.title,
favicons: v.favicons,
isActive: v.isActive,
}))
}
}
export { TabsCommand }
export default TabsCommand

5
src/common/event/common.ts

@ -0,0 +1,5 @@
const keys = ["hot-update-ready"] as const
type AllKeys = (typeof keys)[number]
export type { AllKeys }

14
src/common/event/update/index.ts

@ -0,0 +1,14 @@
// import type { AllKeys } from "../common"
const curProgress = ref(0)
// api.on<AllKeys>("progress", () => {
// curProgress.value = 10
// })
function useUpdate() {
return {
curProgress,
}
}
export { useUpdate }

10
src/common/event/update/main/command.ts

@ -0,0 +1,10 @@
import { inject } from "inversify"
import Updater from "main/modules/updater"
export default class PlatFormCommand {
constructor(@inject(Updater) private _Updater: Updater) {}
async triggerHotUpdate() {
await this._Updater.triggerHotUpdate()
}
}

8
src/common/event/update/main/index.ts

@ -0,0 +1,8 @@
import { broadcast } from "main/utils"
import { AllKeys } from "common/event/common"
function emitHotUpdateReady(...argu) {
broadcast<AllKeys>("hot-update-ready", ...argu)
}
export { emitHotUpdateReady }

12
src/common/lib/_Base.ts

@ -0,0 +1,12 @@
export abstract class _Base {
static instance
static getInstance<T>(): T {
if (!this.instance) {
// 如果实例不存在,则创建一个新的实例
// @ts-ignore ...
this.instance = new this()
}
return this.instance
}
}

53
src/common/lib/abstract.ts

@ -0,0 +1,53 @@
import { ElectronApiClient } from "common/lib/electron"
import { BrowserApiClient } from "common/lib/browser"
// 定义抽象 API 接口
export interface IApiClient {
call<T = any>(command: string, ...args: any[]): Promise<T>
on<K extends string>(channel: K, callback: (...args: any[]) => void): void
off<K extends string>(channel: K, callback: (...args: any[]) => void): void
offAll<K extends string>(channel: K): void
}
class NullApiClient implements IApiClient {
async call<T = any>(command: string, ...args: any[]): Promise<T> {
args
console.warn(`API call to ${command} failed: API client not initialized`)
return undefined as any
}
on<K extends string>(channel: K, callback: (...args: any[]) => void): void {
callback
console.warn(`Failed to register listener for ${channel}: API client not initialized`)
}
off<K extends string>(channel: K, callback: (...args: any[]) => void): void {
callback
console.warn(`Failed to unregister listener for ${channel}: API client not initialized`)
}
offAll<K extends string>(channel: K): void {
console.warn(`Failed to unregister all listeners for ${channel}: API client not initialized`)
}
}
// 创建 API 工厂
export class ApiFactory {
private static instance: IApiClient = new NullApiClient() // 默认使用空实现
static setApiClient(client: IApiClient) {
this.instance = client
}
static getApiClient(): IApiClient {
if (this.instance instanceof NullApiClient) {
// 根据环境选择合适的 API 客户端
if (window.api && window.electron) {
this.instance = new ElectronApiClient()
} else {
this.instance = new BrowserApiClient()
}
}
return this.instance
}
}

29
src/common/lib/browser.ts

@ -0,0 +1,29 @@
import { IApiClient } from "./abstract"
export class BrowserApiClient implements IApiClient {
call<T = any>(command: string, ...args: any[]): Promise<T> {
// 浏览器特定实现,可能使用 fetch 或其他方式
const [service, method] = command.split(".")
return fetch(`/api/${service}/${method}`, {
method: "POST",
body: JSON.stringify(args),
headers: { "Content-Type": "application/json" },
}).then(res => res.json())
}
// 实现其他方法...
on<K extends string>(channel: K, callback: (...args: any[]) => void): void {
// 浏览器中可能使用 WebSocket 或其他方式
console.log("不支持 on 方法", channel, callback)
}
off<K extends string>(channel: K, callback: (...args: any[]) => void): void {
// 相应的解绑实现
console.log("不支持 on 方法", channel, callback)
}
offAll<K extends string>(channel: K): void {
// 相应的全部解绑实现
console.log("不支持 on 方法", channel)
}
}

20
src/common/lib/electron.ts

@ -0,0 +1,20 @@
import { IApiClient } from "./abstract"
export class ElectronApiClient implements IApiClient {
call<T = any>(command: string, ...args: any[]): Promise<T> {
// Electron 特定实现
return window.api.call(command, ...args)
}
on<K extends string>(channel: K, callback: (...args: any[]) => void): void {
window.api.on(channel, callback)
}
off<K extends string>(channel: K, callback: (...args: any[]) => void): void {
window.api.off(channel, callback)
}
offAll<K extends string>(channel: K): void {
window.api.offAll(channel)
}
}

278
src/main/App copy.ts

@ -10,155 +10,155 @@ import { getFileUrl } from "./utils"
import BaseClass from "./base/base"
protocol.registerSchemesAsPrivileged([
// {
// scheme: "http",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// {
// scheme: "https",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// { scheme: "mailto", privileges: { standard: true } },
{
scheme: "api",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
// {
// scheme: "http",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// {
// scheme: "https",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// { scheme: "mailto", privileges: { standard: true } },
{
scheme: "api",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
},
])
@injectable()
class App extends BaseClass {
destroy() {
// destroyAll()
// 这里是应用正常退出
}
// private _setting: Setting
// private _db: DB
private _Api: Api
private _windowManager: WindowManager
// private _tabs: Tabs
destroy() {
// destroyAll()
// 这里是应用正常退出
}
// private _setting: Setting
// private _db: DB
private _Api: Api
private _windowManager: WindowManager
// private _tabs: Tabs
constructor(
// @inject(Setting) setting: Setting,
// @inject(DB) db: DB,
@inject(Api) Api: Api,
@inject(WindowManager) windowManager: WindowManager,
// @inject(Tabs) tabs: Tabs,
) {
super()
// this._setting = setting
// this._db = db
this._Api = Api
this._windowManager = windowManager
// this._tabs = tabs
}
constructor(
// @inject(Setting) setting: Setting,
// @inject(DB) db: DB,
@inject(Api) Api: Api,
@inject(WindowManager) windowManager: WindowManager,
// @inject(Tabs) tabs: Tabs,
) {
super()
// this._setting = setting
// this._db = db
this._Api = Api
this._windowManager = windowManager
// this._tabs = tabs
}
async init() {
this._windowManager.init()
app.whenReady().then(() => {
electronApp.setAppUserModelId("top.xieyaxin")
this.create()
this._Api.init()
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
app.on("will-quit", () => {
this.destroy()
})
}
async init() {
this._windowManager.init()
app.whenReady().then(() => {
electronApp.setAppUserModelId("top.xieyaxin")
this.create()
this._Api.init()
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
app.on("will-quit", () => {
this.destroy()
})
}
create() {
this._windowManager.showMainWindow()
const mainWindow = this._windowManager.getMainWindow()
if (mainWindow) {
nativeTheme.themeSource = "light"
mainWindow.setTitleBarOverlay({
height: 29, // the smallest size of the title bar on windows accounting for the border on windows 11
color: "#F8F8F8",
symbolColor: "#000000",
})
this._windowManager.showWindow("main-top")
const mainTopWindow = this._windowManager.get("main-top")
setTimeout(() => {
// console.log(mainWindow.getParentWindow());
setTimeout(() => {
mainWindow.contentView.children.length = 0
const view = new WebContentsView()
view.addChildView(mainTopWindow!.contentView)
view.webContents.loadURL(getFileUrl("about.html"))
// mainTopWindow!.contentView.setBounds({ x: 0, y: 0, width: 100, height: 30 })
// view.setBounds({ x: 0, y: 0, width: 100, height: 30 })
mainWindow.contentView.addChildView(view)
// mainWindow.contentView.children.sort()
console.log(mainWindow.contentView.children)
}, 5000)
// mainWindow.webContents = mainTopWindow!.webContents
mainWindow.reload()
console.log(mainWindow.webContents.getURL())
create() {
this._windowManager.showMainWindow()
const mainWindow = this._windowManager.getMainWindow()
if (mainWindow) {
nativeTheme.themeSource = "light"
mainWindow.setTitleBarOverlay({
height: 29, // the smallest size of the title bar on windows accounting for the border on windows 11
color: "#F8F8F8",
symbolColor: "#000000",
})
this._windowManager.showWindow("main-top")
const mainTopWindow = this._windowManager.get("main-top")
setTimeout(() => {
// console.log(mainWindow.getParentWindow());
setTimeout(() => {
mainWindow.contentView.children.length = 0
const view = new WebContentsView()
view.addChildView(mainTopWindow!.contentView)
view.webContents.loadURL(getFileUrl("about.html"))
// mainTopWindow!.contentView.setBounds({ x: 0, y: 0, width: 100, height: 30 })
// view.setBounds({ x: 0, y: 0, width: 100, height: 30 })
mainWindow.contentView.addChildView(view)
// mainWindow.contentView.children.sort()
console.log(mainWindow.contentView.children)
}, 5000)
// mainWindow.webContents = mainTopWindow!.webContents
mainWindow.reload()
console.log(mainWindow.webContents.getURL())
// mainTopWindow?.destroy()
// mainWindow.contentView.addChildView(mainWindow.contentView)
console.log(`child count: `, mainWindow.contentView.children.length)
}, 2000)
// if (mainTopWindow) {
// mainTopWindow.setParentWindow(mainWindow)
// mainTopWindow.setIgnoreMouseEvents(true, { forward: false })
// const listenMove = () => {
// if (mainWindow && mainTopWindow) {
// const pos = mainWindow.getPosition()
// mainTopWindow.setPosition(pos[0], pos[1])
// }
// }
// mainWindow?.on("move", listenMove)
// const listenResize = () => {
// if (mainWindow && mainTopWindow) {
// const size = mainWindow.getSize()
// console.log(size)
// mainTopWindow.setSize(size[0], size[1])
// const pos = mainWindow.getPosition()
// mainTopWindow.setPosition(pos[0], pos[1])
// }
// }
// listenResize()
// mainWindow?.on("resize", listenResize)
// }
}
// 考虑双browserwindow模式
/**
* browserwindow可以设置穿透tab放在底层window上window上
*/
// const webContentsView = new WebContentsView({
// webPreferences: {
// preload: join(__dirname, "../preload/index.mjs"),
// transparent: true,
// nodeIntegration: true,
// spellcheck: false,
// contextIsolation: true,
// },
// })
// // mainWindow!.contentView = webContentsView
// // setTimeout(() => {
// mainWindow!.contentView.addChildView(webContentsView)
// // mainWindow?.setIgnoreMouseEvents(true, { forward: true })
// // }, 2000);
// webContentsView.webContents.loadURL(getFileUrl("index.html"))
// const listenResize = () => {
// const size = mainWindow!.getSize()
// webContentsView.setBounds({ x: 0, y: 0, width: size[0], height: size[1] })
// }
// listenResize()
// mainWindow!.addListener("resize", listenResize)
// this._tabs.add("https://baidu.com", true)
// this._tabs.add("https://zhihu.com")
return mainWindow
// mainTopWindow?.destroy()
// mainWindow.contentView.addChildView(mainWindow.contentView)
console.log(`child count: `, mainWindow.contentView.children.length)
}, 2000)
// if (mainTopWindow) {
// mainTopWindow.setParentWindow(mainWindow)
// mainTopWindow.setIgnoreMouseEvents(true, { forward: false })
// const listenMove = () => {
// if (mainWindow && mainTopWindow) {
// const pos = mainWindow.getPosition()
// mainTopWindow.setPosition(pos[0], pos[1])
// }
// }
// mainWindow?.on("move", listenMove)
// const listenResize = () => {
// if (mainWindow && mainTopWindow) {
// const size = mainWindow.getSize()
// console.log(size)
// mainTopWindow.setSize(size[0], size[1])
// const pos = mainWindow.getPosition()
// mainTopWindow.setPosition(pos[0], pos[1])
// }
// }
// listenResize()
// mainWindow?.on("resize", listenResize)
// }
}
// 考虑双browserwindow模式
/**
* browserwindow可以设置穿透tab放在底层window上window上
*/
// const webContentsView = new WebContentsView({
// webPreferences: {
// preload: join(__dirname, "../preload/index.mjs"),
// transparent: true,
// nodeIntegration: true,
// spellcheck: false,
// contextIsolation: true,
// },
// })
// // mainWindow!.contentView = webContentsView
// // setTimeout(() => {
// mainWindow!.contentView.addChildView(webContentsView)
// // mainWindow?.setIgnoreMouseEvents(true, { forward: true })
// // }, 2000);
// webContentsView.webContents.loadURL(getFileUrl("index.html"))
// const listenResize = () => {
// const size = mainWindow!.getSize()
// webContentsView.setBounds({ x: 0, y: 0, width: size[0], height: size[1] })
// }
// listenResize()
// mainWindow!.addListener("resize", listenResize)
// this._tabs.add("https://baidu.com", true)
// this._tabs.add("https://zhihu.com")
return mainWindow
}
}
export default App

127
src/main/App.ts

@ -3,7 +3,7 @@ import { inject, injectable } from "inversify"
// import DB from "./modules/db"
import Api from "./modules/api"
import WindowManager from "./modules/window-manager"
import { app, nativeTheme, protocol } from "electron"
import { app, protocol } from "electron"
import { electronApp } from "@electron-toolkit/utils"
import Command from "./modules/commands"
import BaseClass from "./base/base"
@ -11,79 +11,78 @@ import IOC from "./_ioc"
import DB from "./modules/db"
import Zephyr from "./modules/zephyr"
import Updater from "./modules/updater"
import { crashHandler } from "logger/crash-handler"
protocol.registerSchemesAsPrivileged([
// {
// scheme: "http",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// {
// scheme: "https",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// { scheme: "mailto", privileges: { standard: true } },
{
scheme: "api",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
// {
// scheme: "http",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// {
// scheme: "https",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// { scheme: "mailto", privileges: { standard: true } },
{
scheme: "api",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
{
scheme: "zephyr",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
},
{
scheme: "zephyr",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
},
])
@injectable()
class App extends BaseClass {
destroy() {
this._IOC.destroy()
// 这里是应用正常退出, 可以检测应用是不是非正常退出,比如应用启动时记录一个启动时间并删除上一次结束时间和开始时间,结束时记录一个结束时间,
// 如果存在结束时间或者不存在开始时间则为正常启动
}
destroy() {
this._IOC.destroy()
// 这里是应用正常退出, 可以检测应用是不是非正常退出,比如应用启动时记录一个启动时间并删除上一次结束时间和开始时间,结束时记录一个结束时间,
// 如果存在结束时间或者不存在开始时间则为正常启动
}
constructor(
@inject(IOC) private _IOC: IOC,
@inject(Api) private _Api: Api,
@inject(Command) private _Command: Command,
@inject(DB) private _DB: DB,
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Zephyr) private _Zephyr: Zephyr,
@inject(Updater) private _Updater: Updater,
) {
super()
}
constructor(
@inject(IOC) private _IOC: IOC,
@inject(Api) private _Api: Api,
@inject(Command) private _Command: Command,
@inject(DB) private _DB: DB,
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Zephyr) private _Zephyr: Zephyr,
@inject(Updater) private _Updater: Updater,
) {
super()
}
async init() {
this._Updater.init()
this._DB.init()
this._Command.init()
this._WindowManager.init()
app.whenReady().then(() => {
this._Api.init()
this._Zephyr.init()
electronApp.setAppUserModelId("top.xieyaxin")
this._WindowManager.showMainWindow()
const mainWindow = this._WindowManager.getMainWindow()
if (mainWindow) {
nativeTheme.themeSource = "light"
mainWindow.setTitleBarOverlay({
height: 29, // the smallest size of the title bar on windows accounting for the border on windows 11
color: "#F8F8F8",
symbolColor: "#000000",
})
}
})
app.on("will-quit", () => {
this.destroy()
})
}
async init() {
crashHandler.init()
this._Updater.init()
this._DB.init()
this._Command.init()
this._WindowManager.init()
app.whenReady().then(() => {
this._Api.init()
this._Zephyr.init()
electronApp.setAppUserModelId("top.xieyaxin")
this._WindowManager.showMainWindow()
this._Command.invoke("PlatFormCommand.setTheme", "light")
this._Command.invoke("PlatFormCommand.setTitlBar", {
height: 29,
color: "#F8F8F8",
symbolColor: "#000000",
})
})
app.on("will-quit", () => {
this.destroy()
})
}
}
export default App

8
src/main/_ioc.ts

@ -2,13 +2,13 @@ import IOC from "./_iocClass"
import { Container } from "inversify"
import iocModules, { destroyAllModules } from "./modules/_ioc"
import iocController, { destroyAllController } from "./controller/_ioc"
import iocCommand, { destroyAllCommand } from "./commands/_ioc"
import iocCommand, { destroyAllCommand } from "common/_ioc.main"
import App from "./App"
async function destroyAll() {
await destroyAllModules(_ioc)
await destroyAllController(_ioc)
await destroyAllCommand(_ioc)
await destroyAllModules(_ioc)
await destroyAllController(_ioc)
await destroyAllCommand(_ioc)
}
const _ioc = new Container()

24
src/main/_iocClass.ts

@ -3,21 +3,21 @@ import BaseClass from "./base/base"
import { destroyAll, _ioc } from "./_ioc"
class IOC extends BaseClass {
init() {
// TODO
}
init() {
// TODO
}
destroy() {
destroyAll()
}
destroy() {
destroyAll()
}
get<T = unknown>(serviceIdentifier: interfaces.ServiceIdentifier<T>) {
return _ioc.get<T>(serviceIdentifier)
}
get<T = unknown>(serviceIdentifier: interfaces.ServiceIdentifier<T>) {
return _ioc.get<T>(serviceIdentifier)
}
getAsync<T = unknown>(serviceIdentifier: interfaces.ServiceIdentifier<T>) {
return _ioc.getAsync<T>(serviceIdentifier)
}
getAsync<T = unknown>(serviceIdentifier: interfaces.ServiceIdentifier<T>) {
return _ioc.getAsync<T>(serviceIdentifier)
}
}
export { IOC }

6
src/main/base/base.ts

@ -1,9 +1,9 @@
import EventEmitter from "node:events"
abstract class BaseClass {
public _events = new EventEmitter()
abstract init(...argus: any[])
abstract destroy()
public _events = new EventEmitter()
abstract init(...argus: any[])
abstract destroy()
}
export { BaseClass }

63
src/main/commands/BasicCommand.ts

@ -1,63 +0,0 @@
import { app, dialog } from "electron"
import { inject } from "inversify"
import Tabs from "main/modules/tabs"
import WindowManager from "main/modules/window-manager"
export default class BasicCommand {
constructor(
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Tabs) private _Tabs: Tabs,
) {
//
}
toggleDevTools() {
const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) {
// @ts-ignore ...
focusedWindow.toggleDevTools()
}
}
fullscreen() {
const focusedWindow = this._WindowManager.getFocusWindow()
const isFullScreen = focusedWindow!.isFullScreen()
focusedWindow!.setFullScreen(!isFullScreen)
return !isFullScreen
}
isFullscreen() {
const focusedWindow = this._WindowManager.getFocusWindow()
return focusedWindow!.isFullScreen()
}
relunch() {
app.relaunch()
app.exit()
}
reload() {
const focusedWindow = this._WindowManager.getFocusWindow()
// 重载之后, 刷新并关闭所有的次要窗体
if (this._WindowManager.length() > 1 && focusedWindow && focusedWindow.$$opts!.name === this._WindowManager.mainInfo.name) {
const choice = dialog.showMessageBoxSync(focusedWindow, {
type: "question",
buttons: ["取消", "是的,继续", "不,算了"],
title: "警告",
defaultId: 2,
cancelId: 0,
message: "警告",
detail: "重载主窗口将关闭所有子窗口,是否继续",
})
if (choice == 1) {
this._WindowManager.getWndows().forEach(win => {
if (win.$$opts!.name !== this._WindowManager.mainInfo.name) {
win.close()
}
})
} else {
return
}
}
this._Tabs.closeAll()
focusedWindow!.reload()
}
}

66
src/main/commands/TabsCommand.ts

@ -1,66 +0,0 @@
import { inject } from "inversify"
import Tabs from "main/modules/tabs"
import WindowManager from "main/modules/window-manager"
import { broadcast } from "main/utils"
class TabsCommand {
constructor(
@inject(Tabs) private _Tabs: Tabs,
@inject(WindowManager) private _WindowManager: WindowManager,
) {
this.listenerTabActive = this.listenerTabActive.bind(this)
this._Tabs.events.on("update", this.listenerTabActive)
}
listenerTabActive() {
broadcast("main:TabsCommand.update", this.getAllTabs())
}
bindElement(rect) {
this._Tabs.updateRect(rect)
}
reload() {
this._WindowManager.getMainWindow()?.reload()
}
sync() {
this.listenerTabActive()
if (!this.getAllTabs().length) {
this.add("about:blank")
}
}
add(url) {
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!)
}
nagivate(index: number, url: string) {
this._Tabs.navigate(+index, url)
}
closeAll() {
this._Tabs.closeAll()
}
setActive(index) {
this._Tabs.changeActive(index)
}
closeTab(e) {
this._Tabs.remove(e.body.active)
}
getAllTabs() {
return this._Tabs._tabs.map(v => ({
url: v.url,
showUrl: v.showUrl,
title: v.title,
favicons: v.favicons,
isActive: v.isActive,
}))
}
}
export { TabsCommand }
export default TabsCommand

15
src/main/commands/_ioc.ts

@ -1,15 +0,0 @@
import { Container, ContainerModule } from "inversify"
import BasicCommand from "./BasicCommand"
import TabsCommand from "./TabsCommand"
const modules = new ContainerModule(bind => {
bind("BasicCommand").to(BasicCommand).inSingletonScope()
bind("TabsCommand").to(TabsCommand).inSingletonScope()
})
async function destroyAllCommand(ioc: Container) {
await ioc.unloadAsync(modules)
}
export { modules, destroyAllCommand }
export default modules

30
src/main/controller/BasicService.ts

@ -5,24 +5,24 @@ import WindowManager from "main/modules/window-manager"
@injectable()
class BasicService extends BaseContainer {
constructor(
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Tabs) private _Tabs: Tabs,
) {
super()
}
constructor(
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Tabs) private _Tabs: Tabs,
) {
super()
}
showAbout() {
this._WindowManager.showWindow("about")
return {
a: "fuck",
}
showAbout() {
this._WindowManager.showWindow("about")
return {
a: "fuck",
}
}
openTabDevtool() {
// this._Tabs.reload(0)
this._Tabs.openDevtool(0)
}
openTabDevtool() {
// this._Tabs.reload(0)
this._Tabs.openDevtool(0)
}
}
export { BasicService }

54
src/main/controller/TabsService.ts

@ -5,38 +5,38 @@ import WindowManager from "main/modules/window-manager"
@injectable()
class TabsService extends BaseContainer {
constructor(
@inject(Tabs) private _Tabs: Tabs,
@inject(WindowManager) private _WindowManager: WindowManager,
) {
super()
}
constructor(
@inject(Tabs) private _Tabs: Tabs,
@inject(WindowManager) private _WindowManager: WindowManager,
) {
super()
}
add(e) {
this._Tabs.add(e.body.url, true, this._WindowManager.getMainWindow()!)
}
add(e) {
this._Tabs.add(e.body.url, true, this._WindowManager.getMainWindow()!)
}
setActive(e) {
this._Tabs.changeActive(e.body.active)
}
setActive(e) {
this._Tabs.changeActive(e.body.active)
}
closeTab(e) {
this._Tabs.remove(e.body.active)
}
closeTab(e) {
this._Tabs.remove(e.body.active)
}
closeTabAll(e) {
this._Tabs.removeAll(e.body.active)
}
closeTabAll(e) {
this._Tabs.removeAll(e.body.active)
}
getAllTabs() {
return this._Tabs._tabs.map(v => ({
url: v.url,
showUrl: v.showUrl,
title: v.title,
favicons: v.favicons,
isActive: v.isActive,
}))
}
getAllTabs() {
return this._Tabs._tabs.map(v => ({
url: v.url,
showUrl: v.showUrl,
title: v.title,
favicons: v.favicons,
isActive: v.isActive,
}))
}
}
export { TabsService }

6
src/main/controller/_ioc.ts

@ -3,12 +3,12 @@ import BasicService from "./BasicService"
import TabsService from "./TabsService"
const modules = new ContainerModule(bind => {
bind("BasicService").to(BasicService).inSingletonScope()
bind("TabsService").to(TabsService).inSingletonScope()
bind("BasicService").to(BasicService).inSingletonScope()
bind("TabsService").to(TabsService).inSingletonScope()
})
async function destroyAllController(ioc: Container) {
await ioc.unloadAsync(modules)
await ioc.unloadAsync(modules)
}
export { modules, destroyAllController }

90
src/main/index.ts

@ -1,4 +1,8 @@
import "reflect-metadata"
import "logger/main"
import "logger/main-error"
import "setting/main"
import { _ioc } from "main/_ioc"
import { App } from "main/App"
@ -14,61 +18,45 @@ console.log(`日志地址:${logsPath}`)
const LOG_ROOT = path.join(logsPath)
// 缓存已创建的文件流(避免重复创建)
const streams = new Map()
// 缓存当前应用启动的日志文件流
let currentLogStream: rfs.RotatingFileStream | null = null
// 转换命名空间为安全路径
function sanitizeNamespace(namespace) {
return namespace
.split(":") // 按层级分隔符拆分
.map(part => part.replace(/[\\/:*?"<>|]/g, "_")) // 替换非法字符
.join(path.sep) // 拼接为系统路径分隔符(如 / 或 \)
// 生成当前启动时的日志文件名
const getLogFileName = () => {
const now = new Date()
const timestamp = now.toISOString().replace(/[:.]/g, '-')
return `app-${timestamp}.log`
}
// 覆盖 debug.log 方法
const originalLog = debug.log
debug.log = function (...args) {
// 保留原始控制台输出(可选)
originalLog.apply(this, args)
// 获取当前命名空间
// @ts-ignore ...
const namespace = this.namespace
if (!namespace) {
// TODO 增加容错机制,如果没有命名空间就输出到一个默认文件中
return
}
// 生成日志文件路径(示例:logs/app/server.log)
const sanitizedPath = sanitizeNamespace(namespace)
// const logFilePath = path.join(LOG_ROOT, `${sanitizedPath}.log`)
const today = new Date().toISOString().split("T")[0]
const logFilePath = path.join(LOG_ROOT, sanitizedPath, `${today}.log`)
// 保留原始控制台输出
originalLog.apply(this, args)
// 确保目录存在
const dir = path.dirname(logFilePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }) // 自动创建多级目录
}
// 确保日志目录存在
if (!fs.existsSync(LOG_ROOT)) {
fs.mkdirSync(LOG_ROOT, { recursive: true })
}
// 获取或创建文件流
let stream = streams.get(logFilePath)
if (!stream) {
// stream = fs.createWriteStream(logFilePath, { flags: "a" }) // 追加模式
stream = rfs.createStream(path.parse(logFilePath).base, {
path: dir,
size: "10M", // 单个文件最大 10MB
rotate: 5, // 保留最近 5 个文件
})
streams.set(logFilePath, stream)
}
// 延迟初始化日志流,直到第一次写入
if (!currentLogStream) {
const logFileName = getLogFileName()
currentLogStream = rfs.createStream(logFileName, {
path: LOG_ROOT,
size: "10M", // 单个文件最大 10MB
rotate: 10, // 保留最近 10 个文件
})
}
// 写入日志(添加时间戳)
const message = args.join(" ")
stream.write(`${message}\n`)
// 获取当前命名空间
// @ts-ignore
const namespace = this.namespace || 'unknown'
// const timestamp = new Date().toISOString()
// stream.write(`[${timestamp}] ${message}\n`)
// 写入日志(添加时间戳和命名空间)
const timestamp = new Date().toISOString()
const message = args.join(" ")
currentLogStream.write(`[${timestamp}] [${namespace}] ${message}\n`)
}
const curApp = _ioc.get(App)
@ -76,10 +64,10 @@ curApp.init()
const _debug = debug("app:app")
app.on("before-quit", () => {
_debug("应用关闭")
streams.forEach(stream => {
stream.end()
stream.destroy()
})
streams.clear()
_debug("应用关闭")
if (currentLogStream) {
currentLogStream.end()
currentLogStream.destroy()
currentLogStream = null
}
})

37
src/main/modules/_ioc.ts

@ -1,5 +1,4 @@
import { Container, ContainerModule } from "inversify"
import { Setting } from "./setting"
import { DB } from "./db"
import { Api } from "./api"
import { WindowManager } from "./window-manager"
@ -9,28 +8,26 @@ import Zephyr from "./zephyr"
import Updater from "./updater"
const modules = new ContainerModule(bind => {
bind(Setting).toConstantValue(new Setting())
bind(Zephyr).toSelf().inSingletonScope()
bind(Updater).toSelf().inSingletonScope()
bind(Api).toSelf().inSingletonScope()
bind(WindowManager).toSelf().inSingletonScope()
bind(Commands).toSelf().inSingletonScope()
bind(Tabs).toSelf().inSingletonScope()
bind(DB).toSelf().inSingletonScope()
bind(Zephyr).toSelf().inSingletonScope()
bind(Updater).toSelf().inSingletonScope()
bind(Api).toSelf().inSingletonScope()
bind(WindowManager).toSelf().inSingletonScope()
bind(Commands).toSelf().inSingletonScope()
bind(Tabs).toSelf().inSingletonScope()
bind(DB).toSelf().inSingletonScope()
})
async function destroyAllModules(ioc: Container) {
await Promise.all([
ioc.get(Setting).destroy(),
ioc.get(WindowManager).destroy(),
ioc.get(Commands).destroy(),
ioc.get(Updater).destroy(),
ioc.get(Zephyr).destroy(),
ioc.get(Tabs).destroy(),
ioc.get(Api).destroy(),
ioc.get(DB).destroy(),
])
ioc.unloadAsync(modules)
await Promise.all([
ioc.get(WindowManager).destroy(),
ioc.get(Commands).destroy(),
ioc.get(Updater).destroy(),
ioc.get(Zephyr).destroy(),
ioc.get(Tabs).destroy(),
ioc.get(Api).destroy(),
ioc.get(DB).destroy(),
])
ioc.unloadAsync(modules)
}
export default modules

152
src/main/modules/api/index.ts

@ -5,85 +5,85 @@ import BaseClass from "main/base/base"
@injectable()
class Api extends BaseClass {
constructor(@inject(IOC) private _IOC: IOC) {
super()
this.interceptHandler = this.interceptHandler.bind(this)
}
constructor(@inject(IOC) private _IOC: IOC) {
super()
this.interceptHandler = this.interceptHandler.bind(this)
}
destroy() {
// TODO
}
init(partition?: string) {
// const ses = partition ? session.fromPartition(partition) : session.defaultSession
const ses = partition ? session.fromPartition(partition) : session.defaultSession
ses.protocol.handle("api", this.interceptHandler)
}
async interceptHandler(request: Request) {
if (request.url.startsWith("api://fuck/")) {
let curUrl = request.url
const isScriteText = curUrl.endsWith("?script")
if (isScriteText) {
curUrl = curUrl.replace("?script", "")
}
const isPost = request.method.toLowerCase() === "post"
const file = curUrl.replace("api://fuck/", "")
const array = file.split("/")
const routePath = array.slice(0, -1).join("/")
const fnName = array[array.length - 1]
// https://vitejs.cn/vite5-cn/guide/features.html#dynamic-import
const module = await this._IOC.getAsync(routePath)
// const module = await import(`main/controller/${routePath}.ts`)
const opts = { body: {}, query: {} }
if (isPost) {
opts.body = await request.json()
}
const headers: HeadersInit = {}
if (isScriteText) {
headers["content-type"] = "text/javascript"
}
if (isPost) {
headers["content-type"] = "application/json"
}
if (module && module[fnName]) {
if (typeof module[fnName] === "string") {
const result = module[fnName]
return new Response(result, {
status: 200,
headers: Object.keys(headers).length ? headers : undefined,
})
}
if (typeof module[fnName] === "function") {
let result = await module[fnName](opts)
if (typeof result === "object") {
result = JSON.stringify(result)
}
return new Response(result, {
status: 200,
headers: Object.keys(headers).length ? headers : undefined,
})
}
if (typeof module[fnName] === "object") {
let result = module[fnName]
if (typeof result === "object") {
result = JSON.stringify(result)
}
return new Response(result, {
status: 200,
headers: Object.keys(headers).length ? headers : undefined,
})
}
}
return new Response("", {
status: 500,
headers: Object.keys(headers).length ? headers : undefined,
})
} else if (request.url.startsWith("api://")) {
return new Response("error", {
status: 500,
})
destroy() {
// TODO
}
init(partition?: string) {
// const ses = partition ? session.fromPartition(partition) : session.defaultSession
const ses = partition ? session.fromPartition(partition) : session.defaultSession
ses.protocol.handle("api", this.interceptHandler)
}
async interceptHandler(request: Request) {
if (request.url.startsWith("api://fuck/")) {
let curUrl = request.url
const isScriteText = curUrl.endsWith("?script")
if (isScriteText) {
curUrl = curUrl.replace("?script", "")
}
const isPost = request.method.toLowerCase() === "post"
const file = curUrl.replace("api://fuck/", "")
const array = file.split("/")
const routePath = array.slice(0, -1).join("/")
const fnName = array[array.length - 1]
// https://vitejs.cn/vite5-cn/guide/features.html#dynamic-import
const module = await this._IOC.getAsync(routePath)
// const module = await import(`main/controller/${routePath}.ts`)
const opts = { body: {}, query: {} }
if (isPost) {
opts.body = await request.json()
}
const headers: HeadersInit = {}
if (isScriteText) {
headers["content-type"] = "text/javascript"
}
if (isPost) {
headers["content-type"] = "application/json"
}
if (module && module[fnName]) {
if (typeof module[fnName] === "string") {
const result = module[fnName]
return new Response(result, {
status: 200,
headers: Object.keys(headers).length ? headers : undefined,
})
}
if (typeof module[fnName] === "function") {
let result = await module[fnName](opts)
if (typeof result === "object") {
result = JSON.stringify(result)
}
return new Response(result, {
status: 200,
headers: Object.keys(headers).length ? headers : undefined,
})
}
if (typeof module[fnName] === "object") {
let result = module[fnName]
if (typeof result === "object") {
result = JSON.stringify(result)
}
return new Response(result, {
status: 200,
headers: Object.keys(headers).length ? headers : undefined,
})
}
return net.fetch(request.url, request)
}
return new Response("", {
status: 500,
headers: Object.keys(headers).length ? headers : undefined,
})
} else if (request.url.startsWith("api://")) {
return new Response("error", {
status: 500,
})
}
return net.fetch(request.url, request)
}
}
export default Api

1
src/main/modules/api/readme.md

@ -1,4 +1,3 @@
## 资源
- https://juejin.cn/post/7311619723317657611#heading-6

1
src/main/modules/api/test.ts

@ -69,4 +69,3 @@
// };
// ses.protocol.interceptBufferProtocol("https", interceptHandler);

159
src/main/modules/commands/index.ts

@ -1,4 +1,4 @@
import { IMenuItemOption, IPopupMenuOption } from "#"
import { IMenuItemOption, IPopupMenuOption } from "#/popup-menu"
import { ipcMain, Menu, MenuItem } from "electron"
import { inject } from "inversify"
import IOC from "main/_ioc"
@ -7,86 +7,99 @@ import { isPromise } from "main/utils"
import WindowManager from "../window-manager"
export default class Commands extends BaseClass {
destroy() {
// TODO
}
destroy() {
// TODO
}
constructor(
@inject(IOC) private _IOC: IOC,
@inject(WindowManager) private _WindowManager: WindowManager,
) {
super()
}
constructor(
@inject(IOC) private _IOC: IOC,
@inject(WindowManager) private _WindowManager: WindowManager,
) {
super()
private async handleCommand(command: string, ...argus) {
const splitClass = command.split(".")
const run = await this._IOC.getAsync<any>(splitClass[0])
if (run) {
const result: Promise<any> | any = run[splitClass[1]](...argus)
return [true, result]
}
return [false]
}
init() {
ipcMain.addListener("command", async (event, key, command: string, ...argus) => {
// console.log(event.sender);
try {
const splitClass = command.split(".")
const run = await this._IOC.getAsync<any>(splitClass[0])
if (run) {
const result: Promise<any> | any = run[splitClass[1]](...argus)
if (isPromise(result)) {
result
.then((res: any) => {
event.reply(key, null, res ?? null)
event.returnValue = res ?? null
})
.catch((err: Error) => {
event.reply(key, err)
event.returnValue = null
})
} else {
event.reply(key, null, result ?? null)
event.returnValue = result ?? null
}
} else {
event.reply(key, new Error(`不存在该命令:${command}`))
event.returnValue = null
}
} catch (error) {
event.reply(key, error)
event.returnValue = null
}
})
public async invoke(command, ...argus) {
const result = await this.handleCommand(command, ...argus)
return result
}
ipcMain.on("x_popup_menu", (_, name: string, options: IPopupMenuOption) => {
const menu = new Menu()
const readMenu = (items: IMenuItemOption[]) => {
return items.map(opt => {
if (typeof opt._click_evt === "string") {
const evt: string = opt._click_evt
opt.click = () => {
// broadcast(evt)
this.sendMessage(name, evt)
}
}
if (opt.submenu && Array.isArray(opt.submenu)) {
opt.submenu = readMenu(opt.submenu)
}
init() {
ipcMain.addListener("command", async (event, key, command: string, ...argus) => {
// console.log(event.sender);
try {
const [isExist, result] = await this.handleCommand(command, ...argus)
if (isExist) {
if (isPromise(result)) {
result
.then((res: any) => {
event.reply(key, null, res ?? null)
event.returnValue = res ?? null
})
.catch((err: Error) => {
event.reply(key, err)
event.returnValue = null
})
} else {
event.reply(key, null, result ?? null)
event.returnValue = result ?? null
}
} else {
event.reply(key, new Error(`不存在该命令:${command}`))
event.returnValue = null
}
} catch (error) {
event.reply(key, error)
event.returnValue = null
}
})
return opt
})
ipcMain.on("x_popup_menu", (_, name: string, options: IPopupMenuOption) => {
const menu = new Menu()
const readMenu = (items: IMenuItemOption[]) => {
return items.map(opt => {
if (typeof opt._click_evt === "string") {
const evt: string = opt._click_evt
opt.click = () => {
// broadcast(evt)
this.sendMessage(name, evt)
}
const arrays = readMenu(options.items)
arrays.forEach(v => {
const item = new MenuItem(v)
menu.append(item)
})
}
if (opt.submenu && Array.isArray(opt.submenu)) {
opt.submenu = readMenu(opt.submenu)
}
menu.on("menu-will-close", () => {
this.sendMessage(name, `popup_menu_close:${options.menu_id}`)
// broadcast(`popup_menu_close:${options.menu_id}`)
})
menu.popup(options.popupOptions)
return opt
})
}
}
const arrays = readMenu(options.items)
sendMessage(name: string, evt: string, ...argu: any[]) {
const win = this._WindowManager.get(name)
if (win) {
win.webContents.send(evt, ...argu)
}
arrays.forEach(v => {
const item = new MenuItem(v)
menu.append(item)
})
menu.on("menu-will-close", () => {
this.sendMessage(name, `popup_menu_close:${options.menu_id}`)
// broadcast(`popup_menu_close:${options.menu_id}`)
})
menu.popup(options.popupOptions)
})
}
sendMessage(name: string, evt: string, ...argu: any[]) {
const win = this._WindowManager.get(name)
if (win) {
win.webContents.send(evt, ...argu)
}
}
}

44
src/main/modules/db/custom.ts

@ -3,31 +3,31 @@ import { Low } from "lowdb"
import fs from "fs-extra"
export class CustomAdapter<T> extends JSONFile<T> {
constructor(filepath: string) {
super(filepath)
this.filepath = filepath
constructor(filepath: string) {
super(filepath)
this.filepath = filepath
}
filepath: string = ""
async read() {
if (!fs.existsSync(this.filepath)) {
return null
}
filepath: string = ""
async read() {
if (!fs.existsSync(this.filepath)) {
return null
}
const data = fs.readJSONSync(this.filepath, { throws: false })
if (!data) {
return null
}
return data
const data = fs.readJSONSync(this.filepath, { throws: false })
if (!data) {
return null
}
return data
}
async write(data: T) {
fs.ensureFileSync(this.filepath)
await super.write(data)
}
async write(data: T) {
fs.ensureFileSync(this.filepath)
await super.write(data)
}
}
export class CustomLow<T> extends Low<T> {
constructor(adapter: CustomAdapter<T>, defaultData: T) {
super(adapter, defaultData)
this.filepath = adapter.filepath
}
filepath: string = ""
constructor(adapter: CustomAdapter<T>, defaultData: T) {
super(adapter, defaultData)
this.filepath = adapter.filepath
}
filepath: string = ""
}

120
src/main/modules/db/index.ts

@ -1,5 +1,5 @@
import { inject, injectable } from "inversify"
import Setting from "../setting"
import { injectable } from "inversify"
import Setting from "setting/main"
import { CustomAdapter, CustomLow } from "./custom"
import path from "node:path"
import BaseClass from "main/base/base"
@ -9,73 +9,73 @@ const debug = _debug("app:db")
@injectable()
class DB extends BaseClass {
destroy() {
debug(`DB destroy`)
}
Modules: Record<string, CustomLow<any>> = {}
destroy() {
debug(`DB destroy`)
}
Modules: Record<string, CustomLow<any>> = {}
constructor(@inject(Setting) private _setting: Setting) {
super()
}
constructor() {
super()
}
init() {
console.log("DB Init")
}
init() {
console.log("DB Init")
}
create(filepath) {
const adapter = new CustomAdapter<any>(filepath)
const db = new CustomLow<object>(adapter, {})
db.filepath = filepath
return db
}
create(filepath) {
const adapter = new CustomAdapter<any>(filepath)
const db = new CustomLow<object>(adapter, {})
db.filepath = filepath
return db
}
getDB(dbName: string) {
if (this.Modules[dbName] === undefined) {
const filepath = path.resolve(this._setting.values("storagePath"), "./db/" + dbName + ".json")
this.Modules[dbName] = this.create(filepath)
return this.Modules[dbName]
} else {
const cur = this.Modules[dbName]
const filepath = path.resolve(this._setting.values("storagePath"), "./db/" + dbName + ".json")
if (cur.filepath != filepath) {
this.Modules[dbName] = this.create(filepath)
}
return this.Modules[dbName]
}
getDB(dbName: string) {
if (this.Modules[dbName] === undefined) {
const filepath = path.resolve(Setting.values("storagePath"), "./db/" + dbName + ".json")
this.Modules[dbName] = this.create(filepath)
return this.Modules[dbName]
} else {
const cur = this.Modules[dbName]
const filepath = path.resolve(Setting.values("storagePath"), "./db/" + dbName + ".json")
if (cur.filepath != filepath) {
this.Modules[dbName] = this.create(filepath)
}
return this.Modules[dbName]
}
}
async saveData(data: any): Promise<any>
async saveData(dbName: string, data: any): Promise<any>
async saveData(dbName: string, data?: any): Promise<any> {
let db, rData
if (arguments.length === 2) {
db = this.getDB(dbName)
rData = data
} else {
db = this.getDB("db")
rData = dbName
}
if (db) {
db.data = rData
await db.write()
return db.data
}
return null
async saveData(data: any): Promise<any>
async saveData(dbName: string, data: any): Promise<any>
async saveData(dbName: string, data?: any): Promise<any> {
let db, rData
if (arguments.length === 2) {
db = this.getDB(dbName)
rData = data
} else {
db = this.getDB("db")
rData = dbName
}
if (db) {
db.data = rData
await db.write()
return db.data
}
return null
}
async getData(dbName?: string) {
let db
if (dbName) {
db = this.getDB(dbName)
} else {
db = this.getDB("db")
}
if (db) {
await db.read()
return db.data
}
return null
async getData(dbName?: string) {
let db
if (dbName) {
db = this.getDB(dbName)
} else {
db = this.getDB("db")
}
if (db) {
await db.read()
return db.data
}
return null
}
}
export default DB

235
src/main/modules/setting/index.ts

@ -1,235 +0,0 @@
import fs from "fs-extra"
import { app } from "electron"
import path from "path"
import { cloneDeep } from "lodash"
import { injectable } from "inversify"
import Config from "config"
import _debug from "debug"
import BaseClass from "main/base/base"
const debug = _debug("app:setting")
type IConfig = typeof Config.default_config
type IOnFunc = (n: IConfig, c: IConfig, keys?: (keyof IConfig)[]) => void
type IT = (keyof IConfig)[] | keyof IConfig | "_"
let storagePath = path.join(app.getPath("documents"), Config.app_title)
const storagePathDev = path.join(app.getPath("documents"), Config.app_title + "-dev")
if (process.env.NODE_ENV === "development") {
storagePath = storagePathDev
}
const _tempConfig = cloneDeep(Config.default_config as IConfig)
Object.keys(_tempConfig).forEach(key => {
if (typeof _tempConfig[key] === "string" && _tempConfig[key].includes("$storagePath$")) {
_tempConfig[key] = _tempConfig[key].replace(/\$storagePath\$/g, storagePath)
if (_tempConfig[key] && path.isAbsolute(_tempConfig[key])) {
_tempConfig[key] = path.normalize(_tempConfig[key])
}
}
})
function isPath(str) {
// 使用正则表达式检查字符串是否以斜杠或盘符开头
return /^(?:\/|[a-zA-Z]:\\)/.test(str)
}
function init(config: IConfig) {
// 在配置初始化后执行
Object.keys(config).forEach(key => {
if (config[key] && isPath(config[key]) && path.isAbsolute(config[key])) {
fs.ensureDirSync(config[key])
}
})
// 在配置初始化后执行
// fs.ensureDirSync(config["snippet.storagePath"])
// fs.ensureDirSync(config["bookmark.storagePath"])
}
// 判断是否是空文件夹
function isEmptyDir(fPath: string) {
const pa = fs.readdirSync(fPath)
if (pa.length === 0) {
return true
} else {
return false
}
}
@injectable()
class Setting extends BaseClass {
constructor() {
super()
debug(`Setting inited`)
this.init()
}
destroy() {
// TODO
}
#cb: [IT, IOnFunc][] = []
onChange(fn: IOnFunc, that?: any)
onChange(key: IT, fn: IOnFunc, that?: any)
onChange(fnOrType: IT | IOnFunc, fnOrThat: IOnFunc | any = null, that: any = null) {
if (typeof fnOrType === "function") {
this.#cb.push(["_", fnOrType.bind(fnOrThat)])
} else {
this.#cb.push([fnOrType, fnOrThat.bind(that)])
}
}
#runCB(n: IConfig, c: IConfig, keys: (keyof IConfig)[]) {
for (let i = 0; i < this.#cb.length; i++) {
const temp = this.#cb[i]
const k = temp[0]
const fn = temp[1]
if (k === "_") {
fn(n, c, keys)
}
if (typeof k === "string" && keys.includes(k as keyof IConfig)) {
fn(n, c)
}
if (Array.isArray(k) && k.filter(v => keys.indexOf(v) !== -1).length) {
fn(n, c)
}
}
}
#pathFile: string =
process.env.NODE_ENV === "development"
? path.resolve(app.getPath("userData"), "./config_path-dev")
: path.resolve(app.getPath("userData"), "./config_path")
#config: IConfig = cloneDeep(_tempConfig)
#configPath(storagePath?: string): string {
return path.join(storagePath || this.#config.storagePath, "./config.json")
}
/**
*
* @param confingPath
*/
#syncVar(confingPath?: string) {
const configFile = this.#configPath(confingPath)
if (!fs.pathExistsSync(configFile)) {
fs.ensureFileSync(configFile)
fs.writeJSONSync(configFile, {})
}
const config = fs.readJSONSync(configFile) as IConfig
confingPath && (config.storagePath = confingPath)
// 优先取本地的值
for (const key in config) {
// if (Object.prototype.hasOwnProperty.call(this.#config, key)) {
// this.#config[key] = config[key] || this.#config[key]
// }
// 删除配置时本地的配置不会改变,想一下哪种方式更好
this.#config[key] = config[key] || this.#config[key]
}
}
init() {
debug(`位置:${this.#pathFile}`)
if (fs.pathExistsSync(this.#pathFile)) {
const confingPath = fs.readFileSync(this.#pathFile, { encoding: "utf8" })
if (confingPath && fs.pathExistsSync(this.#configPath(confingPath))) {
this.#syncVar(confingPath)
// 防止增加了配置本地却没变的情况
this.#sync(confingPath)
} else {
this.#syncVar(confingPath)
this.#sync(confingPath)
}
} else {
this.#syncVar()
this.#sync()
}
init.call(this, this.#config)
}
config() {
return this.#config
}
#sync(c?: string) {
const config = cloneDeep(this.#config)
delete config.storagePath
const p = this.#configPath(c)
fs.ensureFileSync(p)
fs.writeJSONSync(this.#configPath(c), config)
}
#change(p: string) {
const storagePath = this.#config.storagePath
if (fs.existsSync(storagePath) && !fs.existsSync(p)) {
fs.moveSync(storagePath, p)
}
if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) {
fs.moveSync(storagePath, p, { overwrite: true })
}
fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" })
}
reset(key: keyof IConfig) {
this.set(key, cloneDeep(_tempConfig[key]))
}
set(key: keyof IConfig | Partial<IConfig>, value?: any) {
const oldMainConfig = Object.assign({}, this.#config)
let isChange = false
const changeKeys: (keyof IConfig)[] = []
const canChangeStorage = (targetPath: string) => {
if (fs.existsSync(oldMainConfig.storagePath) && fs.existsSync(targetPath) && !isEmptyDir(targetPath)) {
if (fs.existsSync(path.join(targetPath, "./config.json"))) {
return true
}
return false
}
return true
}
if (typeof key === "string") {
if (value != undefined && value !== this.#config[key]) {
if (key === "storagePath") {
if (!canChangeStorage(value)) {
throw "无法改变存储地址"
return
}
this.#change(value)
changeKeys.push("storagePath")
this.#config["storagePath"] = value
} else {
changeKeys.push(key)
this.#config[key as string] = value
}
isChange = true
}
} else {
if (key["storagePath"] !== undefined && key["storagePath"] !== this.#config["storagePath"]) {
if (!canChangeStorage(key["storagePath"])) {
throw "无法改变存储地址"
return
}
this.#change(key["storagePath"])
this.#config["storagePath"] = key["storagePath"]
changeKeys.push("storagePath")
isChange = true
}
for (const _ in key) {
if (Object.prototype.hasOwnProperty.call(key, _)) {
const v = key[_]
if (v != undefined && _ !== "storagePath" && v !== this.#config[_]) {
this.#config[_] = v
changeKeys.push(_ as keyof IConfig)
isChange = true
}
}
}
}
if (isChange) {
this.#sync()
this.#runCB(this.#config, oldMainConfig, changeKeys)
}
}
values<T extends keyof IConfig>(key: T): IConfig[T] {
return this.#config[key]
}
}
export default Setting
export { Setting }

8
src/main/modules/tabs/Constant.ts

@ -1,6 +1,6 @@
export function Layout(width, height) {
// Tab布局位置
const NavbarHeight = 30
const OffsetHeight = NavbarHeight + 100
return { x: 0, y: OffsetHeight, width: width, height: height - OffsetHeight }
// Tab布局位置
const NavbarHeight = 30
const OffsetHeight = NavbarHeight + 100
return { x: 0, y: OffsetHeight, width: width, height: height - OffsetHeight }
}

486
src/main/modules/tabs/Tab.ts

@ -3,288 +3,288 @@ import { join } from "node:path"
import BaseClass from "main/base/base"
import _debug from "debug"
// import { Layout } from "./Constant"
import FuckHTML from "res/fuck.html?asset"
import FuckHTML from "@res/fuck.html?asset"
import { fileURLToPath, pathToFileURL } from "node:url"
const debug = _debug("app:tab")
interface IOption {
url: string
active: boolean
url: string
active: boolean
}
interface IRect {
x: number
y: number
width: number
height: number
x: number
y: number
width: number
height: number
}
class Tab extends BaseClass {
init() {
// TODO
}
public url: string = ""
public showUrl: string = ""
public title: string = ""
public favicons: string[] = []
public active: boolean = false
public alive: boolean = false
public isDestory: boolean = false
public playing: boolean = false
public visible: boolean = false
private webContentsView: WebContentsView | null = null
private curWindow: BrowserWindow | null = null
private curRect:
| {
x: number
y: number
width: number
height: number
}
| undefined = undefined
init() {
// TODO
}
public url: string = ""
public showUrl: string = ""
public title: string = ""
public favicons: string[] = []
public active: boolean = false
public alive: boolean = false
public isDestory: boolean = false
public playing: boolean = false
public visible: boolean = false
private webContentsView: WebContentsView | null = null
private curWindow: BrowserWindow | null = null
private curRect:
| {
x: number
y: number
width: number
height: number
}
| undefined = undefined
private defaultOptions: IOption = {
url: "",
active: false,
}
private defaultOptions: IOption = {
url: "",
active: false,
}
private options: IOption
private options: IOption
get isActive() {
return this.active
}
get isActive() {
return this.active
}
get events() {
return this._events
}
get events() {
return this._events
}
constructor(options = {}, window: BrowserWindow, curRect?: IRect) {
super()
this.listenResize = this.listenResize.bind(this)
this.options = {
...this.defaultOptions,
...options,
}
this.url = this.getUrl(this.options.url)
this.showUrl = this.options.url
this.curWindow = window
this.curRect = curRect
this.setActive(this.options.active)
constructor(options = {}, window: BrowserWindow, curRect?: IRect) {
super()
this.listenResize = this.listenResize.bind(this)
this.options = {
...this.defaultOptions,
...options,
}
destroyTimer: NodeJS.Timeout | null = null
stopDestroyTimer() {
if (this.destroyTimer !== null) {
clearTimeout(this.destroyTimer)
this.destroyTimer = null
}
}
startDestroyTimer() {
this.stopDestroyTimer()
if (this.visible) return
if (this.playing) return
this.destroyTimer = setTimeout(() => {
if (this.webContentsView && !this.webContentsView.webContents.isDestroyed()) {
this.curWindow?.contentView.removeChildView(this.webContentsView!)
// @ts-ignore 超过8s没有激活的tab就销毁
this.webContentsView.webContents.destroy()
this.webContentsView = null
this.alive = false
this.events.emit("update")
}
}, 8000)
this.url = this.getUrl(this.options.url)
this.showUrl = this.options.url
this.curWindow = window
this.curRect = curRect
this.setActive(this.options.active)
}
destroyTimer: NodeJS.Timeout | null = null
stopDestroyTimer() {
if (this.destroyTimer !== null) {
clearTimeout(this.destroyTimer)
this.destroyTimer = null
}
}
startDestroyTimer() {
this.stopDestroyTimer()
if (this.visible) return
if (this.playing) return
this.destroyTimer = setTimeout(() => {
if (this.webContentsView && !this.webContentsView.webContents.isDestroyed()) {
this.curWindow?.contentView.removeChildView(this.webContentsView!)
// @ts-ignore 超过8s没有激活的tab就销毁
this.webContentsView.webContents.destroy()
this.webContentsView = null
this.alive = false
this.events.emit("update")
}
}, 8000)
}
setActive(active: boolean) {
if (!active) {
if (!this.webContentsView) return
this.curWindow!.removeListener("resize", this.listenResize)
this.webContentsView.setVisible(false)
this.visible = false
this.startDestroyTimer()
} else {
this.stopDestroyTimer()
this.visible = true
if (!this.webContentsView) {
this.create()
// , this.curWindow!.contentView.children.length - 1
this.curWindow!.contentView.addChildView(this.webContentsView!)
this.alive = true
this.events.emit("update")
}
this.listenResize()
this.curWindow!.addListener("resize", this.listenResize)
this.webContentsView!.setVisible(true)
}
this.active = active
setActive(active: boolean) {
if (!active) {
if (!this.webContentsView) return
this.curWindow!.removeListener("resize", this.listenResize)
this.webContentsView.setVisible(false)
this.visible = false
this.startDestroyTimer()
} else {
this.stopDestroyTimer()
this.visible = true
if (!this.webContentsView) {
this.create()
// , this.curWindow!.contentView.children.length - 1
this.curWindow!.contentView.addChildView(this.webContentsView!)
this.alive = true
this.events.emit("update")
}
this.listenResize()
this.curWindow!.addListener("resize", this.listenResize)
this.webContentsView!.setVisible(true)
}
this.active = active
}
openDevtool() {
if (!this.webContentsView) return
this.webContentsView.webContents.openDevTools({
mode: "right",
})
}
openDevtool() {
if (!this.webContentsView) return
this.webContentsView.webContents.openDevTools({
mode: "right",
})
}
reload() {
if (!this.webContentsView) return
this.webContentsView.webContents.reload()
reload() {
if (!this.webContentsView) return
this.webContentsView.webContents.reload()
}
create() {
let securityAttr: Partial<WebPreferences> = {}
if (this.url.startsWith("file:")) {
// 预加载脚本
securityAttr = {
preload: join(__dirname, "../preload/index.mjs"),
sandbox: false,
}
}
this.webContentsView = new WebContentsView({
webPreferences: {
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
nodeIntegration: false,
spellcheck: false,
contextIsolation: true,
...securityAttr,
},
})
const webContents = this.webContentsView.webContents
this.webContentsView.webContents.loadURL(this.url)
// this.webContentsView.webContents.executeJavaScript(`
// const click = (x,y)=>{
// const ev = new MouseEvent("click", {
// view: window,
// bubbles: true,
// cancelable: true,
// screenX: x,
// screenY: y
// })
create() {
let securityAttr: Partial<WebPreferences> = {}
if (this.url.startsWith("file:")) {
// 预加载脚本
securityAttr = {
preload: join(__dirname, "../preload/index.mjs"),
sandbox: false,
}
}
this.webContentsView = new WebContentsView({
webPreferences: {
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
nodeIntegration: false,
spellcheck: false,
contextIsolation: true,
...securityAttr,
},
})
const webContents = this.webContentsView.webContents
this.webContentsView.webContents.loadURL(this.url)
// this.webContentsView.webContents.executeJavaScript(`
// const click = (x,y)=>{
// const ev = new MouseEvent("click", {
// view: window,
// bubbles: true,
// cancelable: true,
// screenX: x,
// screenY: y
// })
// const el = document.elementFromPoint(x,y);
// console.log(el)
// el.dispatchEvent(ev);
// }
// console.log("点击初始化完成")
// `)
this.webContentsView.webContents.setWindowOpenHandler(ev => {
debug(ev)
this.events.emit("window-open", ev)
return { action: "deny" }
})
webContents.addListener("media-paused", () => {
this.playing = false
this.startDestroyTimer()
})
webContents.addListener("media-started-playing", () => {
this.playing = true
this.stopDestroyTimer()
})
webContents.addListener("did-finish-load", () => {
this.url = webContents.getURL()
this.showUrl = this.getShowUrl(this.url)
this.events.emit("update")
})
webContents.addListener("did-navigate-in-page", () => {
this.url = webContents.getURL()
this.showUrl = this.getShowUrl(this.url)
this.events.emit("update")
})
webContents.addListener("page-title-updated", (_, title) => {
this.title = title
debug(`tab页更新:`, title)
this.events.emit("update")
})
webContents.addListener("page-favicon-updated", (_, favicons) => {
this.favicons = favicons
debug(favicons)
this.events.emit("update")
})
// 待机的销毁,但不去除实例
webContents.addListener("destroyed", () => {
this.#destoryWebContentsView()
})
}
// const el = document.elementFromPoint(x,y);
// console.log(el)
// el.dispatchEvent(ev);
// }
// console.log("点击初始化完成")
// `)
this.webContentsView.webContents.setWindowOpenHandler(ev => {
debug(ev)
this.events.emit("window-open", ev)
return { action: "deny" }
})
webContents.addListener("media-paused", () => {
this.playing = false
this.startDestroyTimer()
})
webContents.addListener("media-started-playing", () => {
this.playing = true
this.stopDestroyTimer()
})
webContents.addListener("did-finish-load", () => {
this.url = webContents.getURL()
this.showUrl = this.getShowUrl(this.url)
this.events.emit("update")
})
webContents.addListener("did-navigate-in-page", () => {
this.url = webContents.getURL()
this.showUrl = this.getShowUrl(this.url)
this.events.emit("update")
})
webContents.addListener("page-title-updated", (_, title) => {
this.title = title
debug(`tab页更新:`, title)
this.events.emit("update")
})
webContents.addListener("page-favicon-updated", (_, favicons) => {
this.favicons = favicons
debug(favicons)
this.events.emit("update")
})
// 待机的销毁,但不去除实例
webContents.addListener("destroyed", () => {
this.#destoryWebContentsView()
})
print() {
return {
url: this.url,
showUrl: this.showUrl,
}
}
print() {
return {
url: this.url,
showUrl: this.showUrl,
}
private getUrl(url) {
if (url === "about:blank") {
debug(FuckHTML)
return pathToFileURL(FuckHTML).href
}
return url
}
private getUrl(url) {
if (url === "about:blank") {
debug(FuckHTML)
return pathToFileURL(FuckHTML).href
}
return url
private getShowUrl(url) {
try {
if (fileURLToPath(url) === FuckHTML) {
debug(url)
debug(FuckHTML)
return "about:blank"
}
} catch (error) {
// ignore
}
return url
}
private getShowUrl(url) {
try {
if (fileURLToPath(url) === FuckHTML) {
debug(url)
debug(FuckHTML)
return "about:blank"
}
} catch (error) {
// ignore
}
return url
listenResize() {
if (!this.curWindow) {
return
}
listenResize() {
if (!this.curWindow) {
return
}
if (!this.webContentsView) {
return
}
if (!this.curRect) {
return
}
this.webContentsView.setBounds(this.curRect)
// const size = this.curWindow.getContentSize()
// this.webContentsView.setBounds(Layout(size[0], size[1]))
if (!this.webContentsView) {
return
}
updateRect(curRect: IRect) {
this.curRect = curRect
if (!this.webContentsView) {
return
}
this.webContentsView.setBounds(this.curRect)
if (!this.curRect) {
return
}
this.webContentsView.setBounds(this.curRect)
// const size = this.curWindow.getContentSize()
// this.webContentsView.setBounds(Layout(size[0], size[1]))
}
navigate(url: string) {
if (!this.webContentsView) return
this.webContentsView.webContents.loadURL(this.getUrl(url))
updateRect(curRect: IRect) {
this.curRect = curRect
if (!this.webContentsView) {
return
}
this.webContentsView.setBounds(this.curRect)
}
#destoryWebContentsView() {
this.stopDestroyTimer()
if (this.webContentsView && this.curWindow && !this.curWindow.isDestroyed()) {
this.curWindow.contentView.removeChildView(this.webContentsView)
this.curWindow.removeListener("resize", this.listenResize)
}
if (this.webContentsView && !this.webContentsView.webContents.isDestroyed()) {
this.webContentsView.webContents.removeAllListeners()
this.webContentsView.removeAllListeners()
// @ts-ignore 超过8s没有激活的tab就销毁
this.webContentsView.webContents.destroy()
}
this.webContentsView = null
}
navigate(url: string) {
if (!this.webContentsView) return
this.webContentsView.webContents.loadURL(this.getUrl(url))
}
destroy() {
this.#destoryWebContentsView()
this.events.removeAllListeners()
this.isDestory = true
debug("Tab destroy")
#destoryWebContentsView() {
this.stopDestroyTimer()
if (this.webContentsView && this.curWindow && !this.curWindow.isDestroyed()) {
this.curWindow.contentView.removeChildView(this.webContentsView)
this.curWindow.removeListener("resize", this.listenResize)
}
if (this.webContentsView && !this.webContentsView.webContents.isDestroyed()) {
this.webContentsView.webContents.removeAllListeners()
this.webContentsView.removeAllListeners()
// @ts-ignore 超过8s没有激活的tab就销毁
this.webContentsView.webContents.destroy()
}
this.webContentsView = null
}
destroy() {
this.#destoryWebContentsView()
this.events.removeAllListeners()
this.isDestory = true
debug("Tab destroy")
}
}
export { Tab }

204
src/main/modules/tabs/index.ts

@ -5,121 +5,121 @@ import { BrowserWindow } from "electron"
import EventEmitter from "events"
interface IRect {
x: number
y: number
width: number
height: number
x: number
y: number
width: number
height: number
}
const debug = _debug("app:tabs")
class Tabs extends BaseClass {
destroy() {
this._tabs.forEach(v => v.destroy())
this._tabs = []
destroy() {
this._tabs.forEach(v => v.destroy())
this._tabs = []
}
public events = new EventEmitter()
private curRect:
| {
x: number
y: number
width: number
height: number
}
| undefined = undefined
constructor() {
super()
}
_tabs: Tab[] = []
init() {
// 初始化
}
updateRect(curRect: IRect) {
this.curRect = curRect
this._tabs.forEach(tab => {
tab.updateRect(curRect)
})
}
add(url: string, active: boolean, win: BrowserWindow) {
const tab = new Tab({ url }, win, this.curRect)
tab.events.on("window-open", ev => {
debug(ev)
this.add(ev.url, true, win)
this.events.emit("update")
// tab.navigate(ev.url)
})
tab.events.on("update", () => {
this.events.emit("update")
})
this._tabs.push(tab)
if (active) {
this.changeActive(this._tabs.length - 1)
}
public events = new EventEmitter()
private curRect:
| {
x: number
y: number
width: number
height: number
}
| undefined = undefined
constructor() {
super()
}
_tabs: Tab[] = []
init() {
// 初始化
}
updateRect(curRect: IRect) {
this.curRect = curRect
this._tabs.forEach(tab => {
tab.updateRect(curRect)
})
}
add(url: string, active: boolean, win: BrowserWindow) {
const tab = new Tab({ url }, win, this.curRect)
tab.events.on("window-open", ev => {
debug(ev)
this.add(ev.url, true, win)
this.events.emit("update")
// tab.navigate(ev.url)
})
tab.events.on("update", () => {
this.events.emit("update")
})
this._tabs.push(tab)
if (active) {
this.changeActive(this._tabs.length - 1)
}
this.events.emit("update")
}
changeActive(index: number) {
this._tabs.forEach((tab, i) => {
tab.setActive(i === index)
})
this.events.emit("update", index)
}
openDevtool(index: number) {
if (this._tabs[index]) {
this._tabs[index].openDevtool()
}
}
reload(index: number) {
if (this._tabs[index]) {
this._tabs[index].reload()
}
}
navigate(index: number, url: string) {
if (this._tabs[index]) {
this._tabs[index].navigate(url)
}
this.events.emit("update")
}
changeActive(index: number) {
this._tabs.forEach((tab, i) => {
tab.setActive(i === index)
})
this.events.emit("update", index)
}
openDevtool(index: number) {
if (this._tabs[index]) {
this._tabs[index].openDevtool()
}
}
remove(index: number) {
this._tabs[index].destroy()
if (this._tabs[index].isActive && index - 1 >= 0) {
this.changeActive(index - 1)
}
this._tabs.splice(index, 1)
this.events.emit("update")
reload(index: number) {
if (this._tabs[index]) {
this._tabs[index].reload()
}
}
closeAll() {
this._tabs = this._tabs.filter(tab => {
tab.destroy()
})
this._tabs = []
this.events.emit("update")
navigate(index: number, url: string) {
if (this._tabs[index]) {
this._tabs[index].navigate(url)
}
}
removeAll(index: number[]) {
index
.map(v => {
return this._tabs[+v]
})
.forEach(tab => {
tab.destroy()
})
this._tabs = this._tabs.filter(v => {
return !v.isDestory
})
this.events.emit("update")
remove(index: number) {
this._tabs[index].destroy()
if (this._tabs[index].isActive && index - 1 >= 0) {
this.changeActive(index - 1)
}
this._tabs.splice(index, 1)
this.events.emit("update")
}
closeAll() {
this._tabs = this._tabs.filter(tab => {
tab.destroy()
})
this._tabs = []
this.events.emit("update")
}
removeAll(index: number[]) {
index
.map(v => {
return this._tabs[+v]
})
.forEach(tab => {
tab.destroy()
})
this._tabs = this._tabs.filter(v => {
return !v.isDestory
})
this.events.emit("update")
}
}
export { Tabs }

118
src/main/modules/updater/hot/index.ts

@ -0,0 +1,118 @@
import { spawn } from "node:child_process"
import fs from "node:fs"
import path from "node:path"
import os from "node:os"
import { app } from "electron"
import extract from "extract-zip"
import { emitHotUpdateReady } from "common/event/Update/main"
import _debug from "debug"
const debug = _debug("app:hot-updater")
function getUpdateScriptTemplate() {
return process.platform === "win32"
? `
@echo off
timeout /t 2
taskkill /IM "{{EXE_NAME}}" /F
xcopy /Y /E "{{UPDATE_DIR}}\\*" "{{APP_PATH}}"
start "" "{{EXE_PATH}}"
`
: `
#!/bin/bash
sleep 2
pkill -f "{{EXE_NAME}}"
cp -Rf "{{UPDATE_DIR}}/*" "{{APP_PATH}}/"
open "{{EXE_PATH}}"
`
}
function generateUpdateScript() {
const scriptContent = getUpdateScriptTemplate()
.replace(/{{APP_PATH}}/g, process.platform === "win32" ? "%APP_PATH%" : "$APP_PATH")
.replace(/{{UPDATE_DIR}}/g, process.platform === "win32" ? "%UPDATE_DIR%" : "$UPDATE_DIR")
.replace(/{{EXE_PATH}}/g, process.platform === "win32" ? "%EXE_PATH%" : "$EXE_PATH")
.replace(/{{EXE_NAME}}/g, process.platform === "win32" ? "%EXE_NAME%" : "$EXE_NAME")
const scriptPath = path.join(os.tmpdir(), `update.${process.platform === "win32" ? "bat" : "sh"}`)
fs.writeFileSync(scriptPath, scriptContent)
return scriptPath
}
// 标记是否需要热更新
let shouldPerformHotUpdate = false
let isReadyUpdate = false
// 更新临时目录路径
// 使用应用名称和随机字符串创建唯一的临时目录
const updateTempDirPath = path.join(os.tmpdir(), `${app.getName()}-update-${Math.random().toString(36).substring(2, 15)}`)
app.once("will-quit", event => {
if (!shouldPerformHotUpdate) return
event.preventDefault()
const appPath = app.getAppPath()
const appExePath = process.execPath
const exeName = path.basename(appExePath)
// 生成动态脚本
const scriptPath = generateUpdateScript()
fs.chmodSync(scriptPath, 0o755)
// 执行脚本
const child = spawn(scriptPath, [], {
detached: true,
shell: true,
env: {
APP_PATH: appPath,
UPDATE_DIR: updateTempDirPath,
EXE_PATH: appExePath,
EXE_NAME: exeName,
},
})
child.unref()
app.exit()
})
// 下载热更新包
export async function fetchHotUpdatePackage(updatePackageUrl: string = "https://example.com/updates/latest.zip") {
if (isReadyUpdate) return
// 清除临时目录
clearUpdateTempDir()
// 创建临时目录
if (!fs.existsSync(updateTempDirPath)) {
fs.mkdirSync(updateTempDirPath, { recursive: true })
}
// 下载文件的本地保存路径
const downloadPath = path.join(updateTempDirPath, "update.zip")
try {
// 使用 fetch 下载更新包
const response = await fetch(updatePackageUrl)
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
}
// 将下载内容写入文件
const arrayBuffer = await response.arrayBuffer()
fs.writeFileSync(downloadPath, Buffer.from(arrayBuffer))
// 解压更新包
await extract(downloadPath, { dir: updateTempDirPath })
// 删除下载的zip文件
fs.unlinkSync(downloadPath)
isReadyUpdate = true
emitHotUpdateReady()
} catch (error) {
debug("热更新包下载失败:", error)
throw error
}
}
function clearUpdateTempDir() {
if (!fs.existsSync(updateTempDirPath)) return
fs.rmSync(updateTempDirPath, { recursive: true })
}
export function flagNeedUpdate() {
shouldPerformHotUpdate = true
}

203
src/main/modules/updater/index.ts

@ -5,110 +5,129 @@ import BaseClass from "main/base/base"
// import { Setting } from "../setting"
import _debug from "debug"
import EventEmitter from "events"
import { fetchHotUpdatePackage, flagNeedUpdate } from "./hot"
import Locales from "locales/main"
const debug = _debug("app:updater")
const { autoUpdater } = pkg
@injectable()
export class Updater extends BaseClass {
public events = new EventEmitter()
constructor(
// @inject(Setting) private _Setting: Setting
) {
super()
// 配置自动更新
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
// 检查更新错误
autoUpdater.on("error", error => {
debug("Update error:", error)
})
// 检查更新
autoUpdater.on("checking-for-update", () => {
debug("Checking for updates...")
})
// 有可用更新
autoUpdater.on("update-available", info => {
debug("Update available:", info)
this.promptUserToUpdate()
})
// 没有可用更新
autoUpdater.on("update-not-available", info => {
debug("Update not available:", info)
})
// 更新下载进度
autoUpdater.on("download-progress", progressObj => {
debug(
`Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`,
)
})
// 更新下载完成
autoUpdater.on("update-downloaded", info => {
debug("Update downloaded:", info)
this.promptUserToInstall()
})
public events = new EventEmitter()
private timer: ReturnType<typeof setInterval> | null = null
// autoReplace = false
async triggerHotUpdate(autoReplace = false) {
await fetchHotUpdatePackage()
flagNeedUpdate()
if (!autoReplace) {
dialog.showMessageBox({
title: Locales.t("update.ready.hot.title"),
message: Locales.t("update.ready.hot.desc", { version: app.getVersion() }),
})
} else {
app.quit()
}
init() {
// 定期检查更新
}
constructor() {
super()
// 配置自动更新
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
// 检查更新错误
autoUpdater.on("error", error => {
debug("Update error:", error)
})
// 检查更新
autoUpdater.on("checking-for-update", () => {
debug("Checking for updates...")
})
// 有可用更新
autoUpdater.on("update-available", info => {
debug("Update available:", info)
this.promptUserToUpdate()
})
// 没有可用更新
autoUpdater.on("update-not-available", info => {
debug("Update not available:", info)
})
// 更新下载进度
autoUpdater.on("download-progress", progressObj => {
debug(
`Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`,
)
})
// 更新下载完成
autoUpdater.on("update-downloaded", info => {
debug("Update downloaded:", info)
this.promptUserToInstall()
})
}
init() {
// 定期检查更新
this.checkForUpdates()
this.timer && clearInterval(this.timer)
this.timer = setInterval(
() => {
this.checkForUpdates()
setInterval(
() => {
this.checkForUpdates()
},
1000 * 60 * 60,
) // 每小时检查一次
}
destroy() {
// 清理工作
},
1000 * 60 * 60,
) // 每小时检查一次
}
destroy() {
// 清理工作
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
private async checkForUpdates() {
if (app.isPackaged) {
try {
await autoUpdater.checkForUpdates()
} catch (error) {
debug("Failed to check for updates:", error)
}
}
}
private async checkForUpdates() {
if (app.isPackaged) {
try {
await autoUpdater.checkForUpdates()
} catch (error) {
debug("Failed to check for updates:", error)
}
}
private async promptUserToUpdate() {
const result = await dialog.showMessageBox({
type: "info",
title: "发现新版本",
message: "是否下载新版本?",
buttons: ["下载", "暂不更新"],
defaultId: 0,
})
if (result.response === 0) {
autoUpdater.downloadUpdate()
}
}
private async promptUserToUpdate() {
const result = await dialog.showMessageBox({
type: "info",
title: "发现新版本",
message: "是否下载新版本?",
buttons: ["下载", "暂不更新"],
defaultId: 0,
})
if (result.response === 0) {
autoUpdater.downloadUpdate()
}
private async promptUserToInstall() {
const result = await dialog.showMessageBox({
type: "info",
title: "更新已就绪",
message: "新版本已下载完成,是否立即安装?",
buttons: ["立即安装", "稍后安装"],
defaultId: 0,
})
if (result.response === 0) {
autoUpdater.quitAndInstall(false, true)
}
}
private async promptUserToInstall() {
const result = await dialog.showMessageBox({
type: "info",
title: "更新已就绪",
message: "新版本已下载完成,是否立即安装?",
buttons: ["立即安装", "稍后安装"],
defaultId: 0,
})
if (result.response === 0) {
autoUpdater.quitAndInstall(false, true)
}
}
}
export default Updater

612
src/main/modules/window-manager/index.ts

@ -4,343 +4,369 @@ import { defaultConfig, defaultWindowConfig, getWindowsMap, IConfig, Param } fro
import { optimizer } from "@electron-toolkit/utils"
import BaseClass from "main/base/base"
import _debug from "debug"
import _logger from "logger/main"
const debug = _debug("app:window-manager")
const logger = _logger.createNamespace("modlue:window-manager") // _debug("app:window-manager")
declare module "electron" {
interface BrowserWindow {
$$forceClose?: boolean
$$lastChoice?: number
$$opts?: Param
}
interface BrowserWindow {
$$forceClose?: boolean
$$lastChoice?: number
$$opts?: Param
}
}
export { WindowManager }
export default class WindowManager extends BaseClass {
constructor() {
super()
}
constructor() {
super()
this.isMainShowReady = new Promise(resolve => {
this.isMainShowResolve = resolve
})
}
destroy() {
// TODO
destroy() {
// TODO
}
globalChioce: number = -1
#showWin(info: Param) {
if (this.#windows.length >= 6) {
dialog.showErrorBox("错误", "窗口数量超出限制")
return
}
globalChioce: number = -1
#showWin(info: Param) {
if (this.#windows.length >= 6) {
dialog.showErrorBox("错误", "窗口数量超出限制")
return
}
if (!info.name) {
dialog.showErrorBox("错误", "窗口未指定唯一key")
return
}
const index = this.findIndex(info.name)
if (index === -1) {
this.#windows.push(this.#add(info))
} else {
if (this.#windows[index].isDestroyed()) {
this.#windows[index] = this.#add(info)
} else {
if (info.url && info.loadURLInSameWin) {
this.#windows[index].loadURL(info.url)
}
this.#windows[index].show()
}
if (!info.name) {
dialog.showErrorBox("错误", "窗口未指定唯一key")
return
}
logger.debug("创建窗口的参数:", info)
const index = this.findIndex(info.name)
if (index === -1) {
this.#windows.push(this.#add(info))
} else {
if (this.#windows[index].isDestroyed()) {
this.#windows[index] = this.#add(info)
} else {
if (info.url && info.loadURLInSameWin) {
this.#windows[index].loadURL(info.url)
}
this.showCurrentWindow()
this.#windows[index].show()
}
}
this.showCurrentWindow()
}
showMainWindow() {
this.#showWin(this.mainInfo)
this.isMainShowResolve()
}
showMainWindow() {
this.#showWin(this.mainInfo)
private isMainShowResolve
private isMainShowReady
async waitMainShowReady() {
await this.isMainShowReady
}
createWindow(name: string, opts?: Partial<IConfig>){
let info = opts as Param
info.name = name
if (!info.ignoreEmptyUrl && !info.url) {
dialog.showErrorBox("错误", name + "窗口未提供url")
return
}
this.#showWin(info as Param)
}
showWindow(name: string, opts?: Partial<IConfig>) {
let have = false
for (const key in this.#urlMap) {
const info = this.#urlMap[key]
if (new RegExp(key).test(name)) {
opts && merge(info, opts)
info.name = name
if (!info.ignoreEmptyUrl && !info.url) {
dialog.showErrorBox("错误", name + "窗口未提供url")
return
}
this.#showWin(info as Param)
have = true
}
}
if (!have) {
dialog.showErrorBox("错误", name + "窗口未创建成功")
return
showWindow(name: string, opts?: Partial<IConfig>) {
let have = false
for (const key in this.#urlMap) {
const info = this.#urlMap[key]
if (new RegExp(key).test(name)) {
opts && merge(info, opts)
info.name = name
if (!info.ignoreEmptyUrl && !info.url) {
dialog.showErrorBox("错误", name + "窗口未提供url")
return
}
this.#showWin(info as Param)
have = true
}
}
init() {
/**
*
*/
app.on("activate", () => {
this.showMainWindow()
})
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window)
})
/**
* event.preventDefault()
* 1. autoUpdater.quitAndInstall()close事件之后执行
* 2. 退
*/
app.on("before-quit", (event: Electron.Event) => {
const mainWin = this.get(this.mainInfo.name)
if (!mainWin || (mainWin && mainWin?.$$forceClose)) {
// app.exit()
} else {
event.preventDefault()
}
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
if (!have) {
dialog.showErrorBox("错误", name + "窗口未创建成功")
return
}
}
#urlMap = getWindowsMap()
init() {
/**
*
*/
app.on("activate", () => {
this.showMainWindow()
})
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window)
})
/**
* event.preventDefault()
* 1. autoUpdater.quitAndInstall()close事件之后执行
* 2. 退
*/
app.on("before-quit", (event: Electron.Event) => {
const mainWin = this.get(this.mainInfo.name)
if (!mainWin || (mainWin && mainWin?.$$forceClose)) {
// app.exit()
} else {
event.preventDefault()
}
})
getWndows() {
return this.#windows
}
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
}
length() {
return this.#windows.length
}
#urlMap = getWindowsMap()
public get mainInfo() {
return this.#urlMap["main"] as Param
}
getWndows() {
return this.#windows
}
#windows: BrowserWindow[] = []
length() {
return this.#windows.length
}
#defaultConfig: IConfig = defaultConfig
public get mainInfo() {
return this.#urlMap["main"] as Param
}
#add(config: Param) {
const curConfig = cloneDeep(this.#defaultConfig ?? {})
for (const key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
const value = config[key]
// if (Reflect.has(curConfig, key)) {
curConfig[key] = value
// }
}
}
const privateConfig = merge(curConfig.overideWindowOpts ? {} : cloneDeep(defaultWindowConfig), curConfig.windowOpts ?? {})
let parentWindow
if (typeof privateConfig.parent === "string") {
parentWindow = this.get(privateConfig.parent)
}
if (parentWindow) {
privateConfig.parent = parentWindow
}
const browserWin = new BrowserWindow(privateConfig)
browserWin.webContents.setWindowOpenHandler(() => {
if (curConfig.denyWindowOpen) {
return { action: "deny" }
}
return { action: "allow" }
})
// @ts-ignore 不需要解释为啥
browserWin.webContents.$$senderName = curConfig.name
browserWin.$$forceClose = false
browserWin.$$lastChoice = -1
browserWin.on("close", (event: any) => {
if (this.globalChioce === 1) {
this.#onClose(curConfig.name)
return
}
if (!curConfig.confrimWindowClose) {
this.#onClose(curConfig.name)
return
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
function justQuit() {
browserWin.$$lastChoice = 1
// app.quit()
// 不要用quit();试了会弹两次
browserWin.$$forceClose = true
if (curConfig.name === that.mainInfo.name) {
that.globalChioce = 1
app.quit() // exit()直接关闭客户端,不会执行quit();
} else {
that.delete(curConfig.name)
that.showCurrentWindow()
}
}
if (browserWin.$$forceClose) {
that.delete(curConfig.name)
app.quit()
} else {
let choice = -1
if (browserWin && browserWin!.$$lastChoice !== undefined && browserWin.$$lastChoice >= 0) {
choice = browserWin.$$lastChoice
} else {
choice = dialog.showMessageBoxSync(browserWin, {
type: "info",
title: curConfig.confrimWindowCloseText.title,
defaultId: curConfig.confrimWindowCloseText.defaultId,
cancelId: curConfig.confrimWindowCloseText.cancelId,
message: curConfig.confrimWindowCloseText.message,
buttons: curConfig.confrimWindowCloseText.buttons,
})
}
if (choice === 1) {
justQuit()
} else {
event && event.preventDefault()
}
}
})
browserWin.$$opts = curConfig
// 在此注册窗口
browserWin.webContents.addListener("did-finish-load", () => {
browserWin.webContents.executeJavaScript(`window._global=${JSON.stringify({ name: curConfig.name })};`)
browserWin.webContents.send("bind-window-manager", curConfig.name)
})
// https://www.electronjs.org/zh/docs/latest/tutorial/security#12-%E5%88%9B%E5%BB%BAwebview%E5%89%8D%E7%A1%AE%E8%AE%A4%E5%85%B6%E9%80%89%E9%A1%B9
// browserWin.webContents.on("will-attach-webview", (_event, webPreferences) => {
// if (webPreferences.preload !== path.resolve(app.getAppPath(), "webview.js")) {
// // 如果未使用,则删除预加载脚本或验证其位置是否合法
// delete webPreferences.preload
// }
#windows: BrowserWindow[] = []
// // 禁用 Node.js 集成
// webPreferences.nodeIntegration = false
#defaultConfig: IConfig = defaultConfig
// // 验证正在加载的 URL
// // if (!params.src.startsWith('https://example.com/')) {
// // event.preventDefault()
// // }
// })
if (curConfig.type === "info") {
// 隐藏菜单
browserWin.setMenuBarVisibility(false)
#add(config: Param) {
const curConfig = cloneDeep(this.#defaultConfig ?? {})
for (const key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
const value = config[key]
// if (Reflect.has(curConfig, key)) {
curConfig[key] = value
// }
}
}
const privateConfig = merge(curConfig.overideWindowOpts ? {} : cloneDeep(defaultWindowConfig), curConfig.windowOpts ?? {})
let parentWindow
if (typeof privateConfig.parent === "string") {
parentWindow = this.get(privateConfig.parent)
}
if (parentWindow) {
privateConfig.parent = parentWindow
}
const browserWin = new BrowserWindow(privateConfig)
browserWin.webContents.setWindowOpenHandler(() => {
if (curConfig.denyWindowOpen) {
return { action: "deny" }
}
return { action: "allow" }
})
// @ts-ignore 不需要解释为啥
browserWin.webContents.$$senderName = curConfig.name
browserWin.$$forceClose = false
browserWin.$$lastChoice = -1
browserWin.on("close", (event: any) => {
if (this.globalChioce === 1) {
this.#onClose(curConfig.name)
return
}
if (!curConfig.confrimWindowClose) {
this.#onClose(curConfig.name)
return
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
function justQuit() {
browserWin.$$lastChoice = 1
// app.quit()
// 不要用quit();试了会弹两次
browserWin.$$forceClose = true
if (curConfig.name === that.mainInfo.name) {
that.globalChioce = 1
app.quit() // exit()直接关闭客户端,不会执行quit();
} else {
that.delete(curConfig.name)
that.showCurrentWindow()
}
if (curConfig.url) {
browserWin.loadURL(curConfig.url)
// logger.debug(`当前窗口网址:${curConfig.url}`)
}
if (browserWin.$$forceClose) {
that.delete(curConfig.name)
app.quit()
} else {
let choice = -1
if (browserWin && browserWin!.$$lastChoice !== undefined && browserWin.$$lastChoice >= 0) {
choice = browserWin.$$lastChoice
} else {
choice = dialog.showMessageBoxSync(browserWin, {
type: "info",
title: curConfig.confrimWindowCloseText.title,
defaultId: curConfig.confrimWindowCloseText.defaultId,
cancelId: curConfig.confrimWindowCloseText.cancelId,
message: curConfig.confrimWindowCloseText.message,
buttons: curConfig.confrimWindowCloseText.buttons,
})
}
if (curConfig.windowOpts?.show === false) {
if (curConfig.url) {
browserWin.once("ready-to-show", () => {
debug(`准备展示:`, curConfig.url)
browserWin?.show()
})
} else {
browserWin?.show()
}
if (choice === 1) {
justQuit()
} else {
event && event.preventDefault()
}
return browserWin
}
}
})
browserWin.$$opts = curConfig
// 在此注册窗口
browserWin.webContents.addListener("did-finish-load", () => {
browserWin.webContents.executeJavaScript(`window._global=${JSON.stringify({ name: curConfig.name })};`)
browserWin.webContents.send("bind-window-manager", curConfig.name)
})
// https://www.electronjs.org/zh/docs/latest/tutorial/security#12-%E5%88%9B%E5%BB%BAwebview%E5%89%8D%E7%A1%AE%E8%AE%A4%E5%85%B6%E9%80%89%E9%A1%B9
// browserWin.webContents.on("will-attach-webview", (_event, webPreferences) => {
// if (webPreferences.preload !== path.resolve(app.getAppPath(), "webview.js")) {
// // 如果未使用,则删除预加载脚本或验证其位置是否合法
// delete webPreferences.preload
// }
showCurrentWindow() {
debug(`current open window: ${this.#windows.map(v => v.$$opts!.name).join(",")}`)
}
// // 禁用 Node.js 集成
// webPreferences.nodeIntegration = false
#onClose(name: string) {
for (let i = this.#windows.length - 1; i >= 0; i--) {
const win = this.#windows[i]
if (name === win.$$opts!.name) {
win.destroy()
this.#windows.splice(i, 1)
}
}
this.showCurrentWindow()
// // 验证正在加载的 URL
// // if (!params.src.startsWith('https://example.com/')) {
// // event.preventDefault()
// // }
// })
if (curConfig.type === "info") {
// 隐藏菜单
browserWin.setMenuBarVisibility(false)
}
get(name: string) {
return this.#windows.find(v => {
return v.$$opts!.name === name
if (curConfig.url) {
browserWin.loadURL(curConfig.url)
// logger.debug(`当前窗口网址:${curConfig.url}`)
}
if (curConfig.windowOpts?.show === false) {
if (curConfig.url) {
browserWin.once("ready-to-show", () => {
logger.debug(`准备展示:`, curConfig.url)
browserWin?.show()
})
} else {
browserWin?.show()
}
}
return browserWin
}
getFocusWindow() {
const mainWindow = this.getMainWindow()
if (mainWindow?.isFocused()) {
return mainWindow
}
for (let i = 0; i < this.#windows.length; i++) {
const win = this.#windows[i]
if (win.isFocused()) {
return win
}
}
return
showCurrentWindow() {
if (this.#windows.length) {
logger.debug(`current open window: ${this.#windows.map(v => v.$$opts!.name).join(",")}`)
} else {
logger.debug(`all closed`)
}
}
getMainWindow() {
return this.#windows.find(v => {
return v.$$opts!.name === this.mainInfo.name
})
#onClose(name: string) {
for (let i = this.#windows.length - 1; i >= 0; i--) {
const win = this.#windows[i]
if (name === win.$$opts!.name) {
win.destroy()
this.#windows.splice(i, 1)
}
}
this.showCurrentWindow()
}
close(name: string | RegExp) {
const indexList = this.findAllIndex(name)
for (let i = indexList.length - 1; i >= 0; i--) {
const index = indexList[i]
const win = this.#windows[index]
win.close()
}
get(name: string) {
return this.#windows.find(v => {
return v.$$opts!.name === name
})
}
getFocusWindow() {
const mainWindow = this.getMainWindow()
if (mainWindow?.isFocused()) {
return mainWindow
}
for (let i = 0; i < this.#windows.length; i++) {
const win = this.#windows[i]
if (win.isFocused()) {
return win
}
}
return
}
delete(name: string | RegExp) {
const indexList = this.findAllIndex(name)
for (let i = indexList.length - 1; i >= 0; i--) {
const index = indexList[i]
this.#windows.splice(index, 1)
}
getMainWindow() {
return this.#windows.find(v => {
return v.$$opts!.name === this.mainInfo.name
})
}
close(name: string | RegExp) {
const indexList = this.findAllIndex(name)
for (let i = indexList.length - 1; i >= 0; i--) {
const index = indexList[i]
const win = this.#windows[index]
win.close()
}
}
findIndex(name: string | RegExp) {
const index = this.#windows.findIndex(v => {
if (typeof name === "string") {
return v.$$opts!.name === name
} else {
return name.test(v.$$opts!.name)
}
})
return index
delete(name: string | RegExp) {
const indexList = this.findAllIndex(name)
for (let i = indexList.length - 1; i >= 0; i--) {
const index = indexList[i]
this.#windows.splice(index, 1)
}
}
findAllIndex(name: string | RegExp) {
const result: number[] = []
for (let i = 0; i < this.#windows.length; i++) {
const win = this.#windows[i]
if (typeof name === "string" && win.$$opts!.name === name) {
result.push(i)
} else if (typeof name !== "string" && name.test(win.$$opts!.name)) {
result.push(i)
}
}
return result
findIndex(name: string | RegExp) {
const index = this.#windows.findIndex(v => {
if (typeof name === "string") {
return v.$$opts!.name === name
} else {
return name.test(v.$$opts!.name)
}
})
return index
}
findAllIndex(name: string | RegExp) {
const result: number[] = []
for (let i = 0; i < this.#windows.length; i++) {
const win = this.#windows[i]
if (typeof name === "string" && win.$$opts!.name === name) {
result.push(i)
} else if (typeof name !== "string" && name.test(win.$$opts!.name)) {
result.push(i)
}
}
return result
}
// show(name: string | RegExp) {
// let indexList = this.findAllIndex(name)
// if (!!indexList.length) {
// for (let i = 0; i < indexList.length; i++) {
// const index = indexList[i];
// const win = this.#windows[index]
// if (win.isDestroyed()) {
// this.#windows[index] = this.#add(win.$$opts)
// } else {
// win.show()
// }
// }
// } else {
// console.warn("该窗口不存在")
// }
// }
// show(name: string | RegExp) {
// let indexList = this.findAllIndex(name)
// if (!!indexList.length) {
// for (let i = 0; i < indexList.length; i++) {
// const index = indexList[i];
// const win = this.#windows[index]
// if (win.isDestroyed()) {
// this.#windows[index] = this.#add(win.$$opts)
// } else {
// win.show()
// }
// }
// } else {
// console.warn("该窗口不存在")
// }
// }
}

201
src/main/modules/window-manager/windowsMap.ts

@ -1,130 +1,107 @@
import config from "config"
import { BrowserWindowConstructorOptions } from "electron"
import { getFileUrl } from "main/utils"
import icon from "res/icon.png?asset"
import { join } from "path"
import { getFileUrl, getPreloadUrl } from "main/utils"
import icon from "@res/icon.png?asset"
export type Param = Partial<IConfig> & Required<Pick<IConfig, "name">>
export interface IConfig {
name?: string
url?: string
loadURLInSameWin?: boolean
type?: "info"
windowOpts?: BrowserWindowConstructorOptions
overideWindowOpts?: boolean
ignoreEmptyUrl?: boolean
denyWindowOpen?: boolean
confrimWindowClose?: boolean
confrimWindowCloseText?: {
title: string
message: string
buttons: string[]
defaultId: number
cancelId: number
}
name?: string
url?: string
loadURLInSameWin?: boolean
type?: "info"
windowOpts?: BrowserWindowConstructorOptions
overideWindowOpts?: boolean
ignoreEmptyUrl?: boolean
denyWindowOpen?: boolean
confrimWindowClose?: boolean
confrimWindowCloseText?: {
title: string
message: string
buttons: string[]
defaultId: number
cancelId: number
}
}
export const defaultConfig: IConfig = {
denyWindowOpen: true,
denyWindowOpen: true,
}
export const defaultWindowConfig = {
height: 600,
useContentSize: true,
width: 800,
show: true,
resizable: true,
minWidth: 900,
minHeight: 600,
frame: true,
transparent: false,
alwaysOnTop: false,
webPreferences: {},
height: 600,
useContentSize: true,
width: 800,
show: true,
resizable: true,
minWidth: 900,
minHeight: 600,
frame: true,
transparent: false,
alwaysOnTop: false,
webPreferences: {},
}
export function getWindowsMap(): Record<string, IConfig> {
return {
main: {
name: "main",
url: getFileUrl("index.html"),
confrimWindowClose: true,
confrimWindowCloseText: {
title: config.app_title,
defaultId: 0,
cancelId: 0,
message: "确定要关闭吗?",
buttons: ["没事", "直接退出"],
},
windowOpts: {
show: false,
titleBarStyle: "hidden",
titleBarOverlay: true,
icon: icon,
...(process.platform === "linux" ? { icon } : {}),
webPreferences: {
webviewTag: false,
preload: join(__dirname, "../preload/index.mjs"),
nodeIntegration: true,
contextIsolation: true,
},
},
return {
main: {
name: "main",
url: getFileUrl("index.html"),
confrimWindowClose: true,
confrimWindowCloseText: {
title: config.app_title,
defaultId: 0,
cancelId: 0,
message: "确定要关闭吗?",
buttons: ["没事", "直接退出"],
},
windowOpts: {
show: false,
titleBarStyle: "hidden",
titleBarOverlay: true,
icon: icon,
...(process.platform === "linux" ? { icon } : {}),
webPreferences: {
webviewTag: false,
preload: getPreloadUrl("index"),
nodeIntegration: true,
contextIsolation: true,
},
_blank: {
overideWindowOpts: false,
confrimWindowClose: true,
confrimWindowCloseText: {
title: config.app_title,
defaultId: 0,
cancelId: 0,
message: "确定要关闭吗?",
buttons: ["没事", "直接退出"],
},
type: "info",
windowOpts: {
height: 600,
useContentSize: true,
width: 800,
show: true,
resizable: true,
minWidth: 900,
minHeight: 600,
frame: true,
transparent: false,
alwaysOnTop: false,
icon: icon,
title: config.app_title,
webPreferences: {
devTools: false,
sandbox: true,
nodeIntegration: false,
contextIsolation: true,
webviewTag: false,
preload: undefined,
},
},
},
},
_blank: {
overideWindowOpts: false,
confrimWindowClose: true,
confrimWindowCloseText: {
title: config.app_title,
defaultId: 0,
cancelId: 0,
message: "确定要关闭吗?",
buttons: ["没事", "直接退出"],
},
type: "info",
windowOpts: {
height: 600,
useContentSize: true,
width: 800,
show: true,
resizable: true,
minWidth: 900,
minHeight: 600,
frame: true,
transparent: false,
alwaysOnTop: false,
icon: icon,
title: config.app_title,
webPreferences: {
devTools: false,
sandbox: true,
nodeIntegration: false,
contextIsolation: true,
webviewTag: false,
preload: undefined,
},
"^about": {
url: getFileUrl("about.html"),
overideWindowOpts: true,
confrimWindowClose: false,
type: "info",
windowOpts: {
width: 600,
height: 200,
minimizable: false,
darkTheme: true,
modal: true,
show: false,
resizable: false,
icon: icon,
webPreferences: {
devTools: false,
sandbox: false,
nodeIntegration: false,
contextIsolation: true,
},
},
},
}
},
},
}
}

497
src/main/modules/zephyr/index.ts

@ -2,48 +2,475 @@ 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
) {
super()
this.interceptHandlerZephyr = this.interceptHandlerZephyr.bind(this)
debug("zephyr init")
}
destroy() {
// TODO
}
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,
})
}
}
// 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() {
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)
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)
}
}
}
export default Zephyr

34
src/main/utils/index.ts

@ -3,27 +3,31 @@ import { join } from "node:path"
import { webContents } from "electron"
export function getFileUrl(app: string) {
let winURL = ""
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
winURL = process.env["ELECTRON_RENDERER_URL"] + `/${app}#/`
} else {
winURL = join(__dirname, `../renderer/${app}#/`)
}
return slash(winURL)
let winURL = ""
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
winURL = process.env["ELECTRON_RENDERER_URL"] + `/${app}#/`
} else {
winURL = join(__dirname, `../renderer/${app}#/`)
}
return slash(winURL)
}
export function getPreloadUrl(file){
return join(__dirname, `../preload/${file}.mjs`)
}
export function isPromise(value: () => any) {
return value && Object.prototype.toString.call(value) === "[object Promise]"
return value && Object.prototype.toString.call(value) === "[object Promise]"
}
export const broadcast = (event: string, ...args: any[]) => {
webContents.getAllWebContents().forEach(browser => browser.send(event, ...args))
export const broadcast = <T extends string>(event: T, ...args: any[]) => {
webContents.getAllWebContents().forEach(browser => browser.send(event, ...args))
}
export function slash(path: string) {
const isExtendedLengthPath = path.startsWith("\\\\?\\")
if (isExtendedLengthPath) {
return path
}
return path.replace(/\\/g, "/")
const isExtendedLengthPath = path.startsWith("\\\\?\\")
if (isExtendedLengthPath) {
return path
}
return path.replace(/\\/g, "/")
}

106
src/preload/call.ts

@ -2,70 +2,70 @@ import { ipcRenderer } from "electron"
let count = 0
export function call(command: string, ...args: any[]): Promise<any> {
return new Promise((resolve, reject) => {
if (!command) {
console.warn("命令不能为空")
return
}
count++
const timestamp = new Date().getTime()
const key = timestamp + "-" + count
let timeID: any = null
ipcRenderer.once(key, fn)
return new Promise((resolve, reject) => {
if (!command) {
console.warn("命令不能为空")
return
}
count++
const timestamp = new Date().getTime()
const key = timestamp + "-" + count
let timeID: any = null
ipcRenderer.once(key, fn)
function fn(_, err: any, res: any) {
clearTimeout(timeID)
if (err) {
reject(err)
return
}
resolve(res)
}
function fn(_, err: any, res: any) {
clearTimeout(timeID)
if (err) {
reject(err)
return
}
resolve(res)
}
ipcRenderer.send("command", key, command, ...args)
ipcRenderer.send("command", key, command, ...args)
// 超过5s就取消监听
timeID = setTimeout(() => {
reject(new Error(`超过5s未响应: ${command}`))
ipcRenderer.removeListener(key, fn)
}, 5000)
})
// 超过5s就取消监听
timeID = setTimeout(() => {
reject(new Error(`超过5s未响应: ${command}`))
ipcRenderer.removeListener(key, fn)
}, 5000)
})
}
export function callLong(command: string, ...args: any[]): Promise<any> {
return new Promise((resolve, reject) => {
if (!command) {
console.warn("命令不能为空")
return
}
count++
const timestamp = new Date().getTime()
const key = timestamp + "-" + count
ipcRenderer.once(key, fn)
function fn(_, err: any, res: any) {
if (err) {
reject(err)
return
}
resolve(res)
}
ipcRenderer.send("command", key, command, ...args)
})
}
export function callSync(command: string, ...args: any[]) {
return new Promise((resolve, reject) => {
if (!command) {
console.warn("命令不能为空")
return
console.warn("命令不能为空")
return
}
count++
const timestamp = new Date().getTime()
const key = timestamp + "-" + count
const result = ipcRenderer.sendSync("command", key, command, ...args)
if (!result) {
ipcRenderer.once(key, fn)
function fn(_, err: any, res: any) {
if (err) {
reject(err)
return
}
resolve(res)
}
return result
ipcRenderer.send("command", key, command, ...args)
})
}
export function callSync(command: string, ...args: any[]) {
if (!command) {
console.warn("命令不能为空")
return
}
count++
const timestamp = new Date().getTime()
const key = timestamp + "-" + count
const result = ipcRenderer.sendSync("command", key, command, ...args)
if (!result) {
return
}
return result
}

8
src/preload/index.d.ts

@ -1,8 +0,0 @@
import { ElectronAPI } from "@electron-toolkit/preload"
declare global {
interface Window {
electron: ElectronAPI
api: unknown
}
}

88
src/preload/index.ts

@ -1,10 +1,12 @@
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron"
import { electronAPI } from "@electron-toolkit/preload"
import { call, callLong, callSync } from "./call"
import { IPopupMenuOption } from "#"
import "logger/preload"
import "logger/preload-error"
import { IPopupMenuOption } from "#/popup-menu"
document.addEventListener("DOMContentLoaded", () => {
const initStyle = document.createElement("style")
initStyle.textContent = `
const initStyle = document.createElement("style")
initStyle.textContent = `
*,
*::before,
*::after {
@ -19,59 +21,59 @@ body {
// background: #F8F8F8;
}
`
document.head.appendChild(initStyle)
document.head.appendChild(initStyle)
})
// Custom APIs for renderer
const api = {
call,
callLong,
callSync,
send(command: string, ...argu: any[]) {
if (!command) return
return ipcRenderer.send(command, ...argu)
},
sendSync(command: string, ...argu: any[]) {
if (!command) return
return ipcRenderer.sendSync(command, ...argu)
},
on(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.on(command, cb)
return () => ipcRenderer.removeListener(command, cb)
},
once(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.once(command, cb)
return () => ipcRenderer.removeListener(command, cb)
},
off(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
return ipcRenderer.removeListener(command, cb)
},
offAll(command: string) {
return ipcRenderer.removeAllListeners(command)
},
popupMenu(options: IPopupMenuOption) {
ipcRenderer.send("x_popup_menu", curWebContentName, options)
},
call,
callLong,
callSync,
send(command: string, ...argu: any[]) {
if (!command) return
return ipcRenderer.send(command, ...argu)
},
sendSync(command: string, ...argu: any[]) {
if (!command) return
return ipcRenderer.sendSync(command, ...argu)
},
on(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.on(command, cb)
return () => ipcRenderer.removeListener(command, cb)
},
once(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.once(command, cb)
return () => ipcRenderer.removeListener(command, cb)
},
off(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
return ipcRenderer.removeListener(command, cb)
},
offAll(command: string) {
return ipcRenderer.removeAllListeners(command)
},
popupMenu(options: IPopupMenuOption) {
ipcRenderer.send("x_popup_menu", curWebContentName, options)
},
}
let curWebContentName = ""
ipcRenderer.once("bind-window-manager", (_, name: string) => {
curWebContentName = name
curWebContentName = name
})
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld("electron", electronAPI)
contextBridge.exposeInMainWorld("api", api)
} catch (error) {
console.error(error)
}
try {
contextBridge.exposeInMainWorld("electron", electronAPI)
contextBridge.exposeInMainWorld("api", api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}

50
src/renderer/about.html

@ -1,28 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>关于我</title>
<style>
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
outline: none;
border: 0;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>关于我</title>
<style>
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
outline: none;
border: 0;
overflow: hidden;
padding: 10px 20px;
}
</style>
</head>
<body>
<article>
<h1 id="demo" style="text-align: center">您好,亲爱的冒险者!</h1>
</article>
</body>
<body>
<article>
<h1>环境</h1>
<ul>
<li>111</li>
</ul>
</article>
</body>
</html>

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']
@ -477,6 +478,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']

27
src/renderer/index.html

@ -1,26 +1,27 @@
<!doctype html>
<html>
<head>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' api: 'unsafe-inline';
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' api: 'unsafe-inline';
script-src 'self' api:;
style-src 'self' 'unsafe-inline';
img-src 'self' data: *;" />
</head>
img-src 'self' data: *;"
/>
</head>
<body>
<body>
<div id="app"></div>
<noscript>
<style>
[data-simplebar] {
overflow: auto;
}
</style>
<style>
[data-simplebar] {
overflow: auto;
}
</style>
</noscript>
<script type="module" src="/src/main.ts"></script>
</body>
</body>
</html>

BIN
src/renderer/public/icons/default_file.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/default_folder.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/default_folder_opened.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/default_root_folder.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/default_root_folder_opened.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_access.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_access2.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_actionscript.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_actionscript2.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_ada.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_advpl.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_affectscript.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_affinitydesigner.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_affinityphoto.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_affinitypublisher.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_agda.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_ai.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_ai2.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_al.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_al_dal.svg (Stored with Git LFS)

Binary file not shown.

BIN
src/renderer/public/icons/file_type_allcontributors.svg (Stored with Git LFS)

Binary file not shown.

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save