Compare commits

...

29 Commits

Author SHA1 Message Date
谢亚昕 e969ec2236 优化事件系统和配置管理,增强日志功能 1 day ago
谢亚昕 a9de1ec525 重构项目架构并优化热更新功能 3 days ago
谢亚昕 fcad3681b4 feat: 添加fs-extra依赖并优化代码结构 4 weeks ago
谢亚昕 f19c097001 add 1 month ago
谢亚昕 81f76353f6 add 2 months ago
npmrun 068777d914 refactor: 移除law-ui依赖并优化代码结构 2 months ago
谢亚昕 34a762ad2c cookie测试 2 months ago
谢亚昕 23893adc43 feat 2 months ago
谢亚昕 6692e16720 优化 2 months ago
谢亚昕 de5f511d6a fix bug 2 months ago
npmrun d4be8b22b9 feat(崩溃处理): 添加崩溃处理模块以捕获并报告应用崩溃 2 months ago
npmrun c142937af9 refactor(logger): 重构错误处理逻辑,分离渲染进程和preload进程的错误处理 2 months ago
npmrun 950bfe9060 feat(logger): 添加错误处理模块并集成到主进程和渲染进程 2 months ago
npmrun 7035429775 feat(logger): 添加固定命名空间的日志记录功能 2 months ago
npmrun 05f83e2a08 feat: 添加logger和setting模块并重构日志系统 2 months ago
npmrun fa6ef80493 Merge branch 'feat/优化' of ssh://git.xieyaxin.top:8892/topuser/electron-app into feat/优化 2 months ago
npmrun 0f093b2ef9 refactor(PlatForm): 重构 PlatForm 相关代码并添加重载功能 2 months ago
谢亚昕 80cc4fe0fe fix bug 2 months ago
npmrun 28eea56a3d refactor(config): 导出 IDefaultConfig 接口以重用类型定义 2 months ago
npmrun b6964f5fbe style: 统一代码缩进为2个空格,提升代码可读性 2 months ago
npmrun 7246ab2d9a refactor(命令模块): 将命令模块从主进程迁移到通用模块,并重构相关代码 2 months ago
npmrun bd9ac214c6 feat(updater): 实现热更新功能并优化命令处理 2 months ago
谢亚昕 dcdc4aa857 fix bug 2 months ago
npmrun 2d5a57853d feat 2 months ago
谢亚昕 3c434df31c 修改了一些东西 2 months ago
npmrun b4b975174d feat: 增加很多功能 3 months ago
npmrun 91f06eb4a1 feat: 优化 3 months ago
谢亚昕 248716be69 feat: 优化界面 3 months ago
npmrun ca363ceac9 feat: 优化 3 months ago
  1. 2
      .editorconfig
  2. 47
      .eslintrc.cjs
  3. 41
      .prettierrc
  4. 2
      .vscode/extensions.json
  5. 74
      .vscode/launch.json
  6. 32
      .vscode/settings.json
  7. 81
      config/index.ts
  8. 62
      electron-builder.yml
  9. 180
      electron.vite.config.ts
  10. 156
      package.json
  11. 123
      packages/base/event/main/index copy.ts
  12. 51
      packages/base/event/main/index.ts
  13. 27
      packages/base/index.ts
  14. 13
      packages/base/package.json
  15. 13
      packages/helper/package.json
  16. 7
      packages/helper/updater/common.ts
  17. 8
      packages/helper/updater/main/handler.ts
  18. 74
      packages/helper/updater/main/hot/download.ts
  19. 118
      packages/helper/updater/main/hot/index.ts
  20. 137
      packages/helper/updater/main/index.ts
  21. 0
      packages/helper/updater/renderer.ts
  22. 41
      packages/locales/index.ts
  23. 20
      packages/locales/languages/en.json
  24. 20
      packages/locales/languages/zh.json
  25. 53
      packages/locales/main.ts
  26. 7
      packages/locales/package.json
  27. 31
      packages/logger/common.ts
  28. 486
      packages/logger/crash-handler.ts
  29. 177
      packages/logger/main-error.ts
  30. 275
      packages/logger/main.ts
  31. 7
      packages/logger/package.json
  32. 195
      packages/logger/preload-error.ts
  33. 153
      packages/logger/preload.ts
  34. 243
      packages/logger/renderer-error.ts
  35. 231
      packages/setting/main.ts
  36. 6
      packages/setting/main/event.ts
  37. 7
      packages/setting/package.json
  38. 928
      pnpm-lock.yaml
  39. 2
      pnpm-workspace.yaml
  40. 17
      resources/fuck.html
  41. 34
      src/common/_ioc.main.ts
  42. 5
      src/common/event/PlatForm/hook.ts
  43. 58
      src/common/event/PlatForm/index.ts
  44. 121
      src/common/event/PlatForm/main/command.ts
  45. 5
      src/common/event/Snippet/hook.ts
  46. 18
      src/common/event/Snippet/index.ts
  47. 44
      src/common/event/Snippet/main/command.ts
  48. 54
      src/common/event/Tabs/index.ts
  49. 65
      src/common/event/Tabs/main/command.ts
  50. 15
      src/common/event/Updater/index.ts
  51. 15
      src/common/event/Updater/main/command.ts
  52. 53
      src/common/lib/abstract.ts
  53. 29
      src/common/lib/browser.ts
  54. 20
      src/common/lib/electron.ts
  55. 7
      src/common/readme.md
  56. 278
      src/main/App copy.ts
  57. 131
      src/main/App.ts
  58. 8
      src/main/_ioc.ts
  59. 24
      src/main/_iocClass.ts
  60. 130
      src/main/base/LinkedList.ts
  61. 56
      src/main/base/base.ts
  62. 63
      src/main/commands/BasicCommand.ts
  63. 66
      src/main/commands/TabsCommand.ts
  64. 15
      src/main/commands/_ioc.ts
  65. 30
      src/main/controller/BasicService.ts
  66. 54
      src/main/controller/TabsService.ts
  67. 24
      src/main/controller/_ioc.ts
  68. 60
      src/main/debug.ts
  69. 6
      src/main/event.ts
  70. 84
      src/main/index.ts
  71. 36
      src/main/modules/_ioc.ts
  72. 152
      src/main/modules/api/index.ts
  73. 1
      src/main/modules/api/readme.md
  74. 1
      src/main/modules/api/test.ts
  75. 159
      src/main/modules/commands/index.ts
  76. 44
      src/main/modules/db/custom.ts
  77. 124
      src/main/modules/db/index.ts
  78. 235
      src/main/modules/setting/index.ts
  79. 8
      src/main/modules/tabs/Constant.ts
  80. 488
      src/main/modules/tabs/Tab.ts
  81. 206
      src/main/modules/tabs/index.ts
  82. 114
      src/main/modules/updater/index.ts
  83. 613
      src/main/modules/window-manager/index.ts
  84. 201
      src/main/modules/window-manager/windowsMap.ts
  85. 500
      src/main/modules/zephyr/index.ts
  86. 34
      src/main/utils/index.ts
  87. 47
      src/main/utils/session/cookies.ts
  88. 82
      src/main/utils/session/index.ts
  89. 106
      src/preload/call.ts
  90. 8
      src/preload/index.d.ts
  91. 88
      src/preload/index.ts
  92. 0
      src/preload/plugin.ts
  93. 54
      src/renderer/about.html
  94. 2
      src/renderer/auto-imports.d.ts
  95. 16
      src/renderer/components.d.ts
  96. 27
      src/renderer/index.html
  97. 35
      src/renderer/src/App.vue
  98. 427
      src/renderer/src/assets/libs/scrollbot.ts
  99. 39
      src/renderer/src/assets/style/_common.scss
  100. 116
      src/renderer/src/bridge/PopupMenu.ts

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 } = require("node:fs")
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
const prettierConfig = JSON.parse(readFileSync("./.prettierrc", { 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],
},
}

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
}

81
config/index.ts

@ -1,35 +1,52 @@
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.hoturl": string
"update.repo"?: string
"update.owner"?: string
"update.allowDowngrade": boolean
"update.allowPrerelease": boolean
"editor.bg": string
"editor.logoType": LogoType
"editor.fontFamily": string
"snippet.storagePath": 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.hoturl":
"https://alist.xieyaxin.top/d/%E8%B5%84%E6%BA%90/%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.zip?sign=eqy35CR-J1SOQZz0iUN2P3B0BiyZPdYH0362nLXbUhE=:1749085071",
"update.repo": "wood-desktop",
"update.owner": "npmrun",
"update.allowDowngrade": false,
"update.allowPrerelease": false,
"snippet.storagePath": "$storagePath$/snippets",
},
} 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/

180
electron.vite.config.ts

@ -9,86 +9,120 @@ 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"
import IconsResolver from "unplugin-icons/resolver"
import Icons from "unplugin-icons/vite"
export default defineConfig({
main: {
resolve: {
alias: {
config: resolve("config"),
main: resolve("src/main"),
res: resolve("resources"),
},
main: {
resolve: {
alias: {
config: resolve("config"),
main: resolve("src/main"),
common: resolve("src/common"),
"@res": resolve("resources"),
},
},
plugins: [externalizeDepsPlugin()],
},
preload: {
build: {
lib: {
entry: {
index: resolve(__dirname, "./src/preload/index.ts"),
plugin: resolve(__dirname, "./src/preload/plugin.ts"),
},
plugins: [externalizeDepsPlugin()],
},
},
preload: {
plugins: [externalizeDepsPlugin()],
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", "src/ui"],
resolvers: [
IconsResolver({
prefix: "icon",
}),
],
}),
Icons(),
// https://wf0.github.io/example/plugins/Formatter.html
// @ts-ignore ...
monacoEditorPlugin.default({
publicPath: "monacoeditorwork",
customDistPath() {
return resolve(__dirname, "out/renderer/monacoeditorwork")
},
}),
],
},
})

156
package.json

@ -1,71 +1,89 @@
{
"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",
"fs-extra": "^11.3.0",
"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",
"@iconify/json": "^2.2.324",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@rushstack/eslint-patch": "^1.10.5",
"@types/debug": "^4.1.12",
"@types/node": "^20.17.19",
"@types/nprogress": "^0.2.3",
"@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",
"base": "workspace:*",
"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",
"helper": "workspace:*",
"locales": "workspace:*",
"lodash-es": "^4.17.21",
"logger": "workspace:^",
"monaco-editor": "^0.52.2",
"nprogress": "^0.2.0",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
"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-icons": "^22.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",
"vue": "^3.5.13",
"vue-i18n": "^11.1.1",
"vue-router": "^4.5.0",
"vue-tsc": "^2.1.10"
}
}

123
packages/base/event/main/index copy.ts

@ -0,0 +1,123 @@
type FireFN = (...argu: any[]) => void
// 监听器类型定义,支持优先级
interface Listener<F extends FireFN> {
fn: F
once: boolean
priority: number
}
class FireEvent<T extends Record<string | symbol, FireFN>> {
// 使用 Map 存储事件监听器,支持 symbol 键
private events = new Map<keyof T, Array<Listener<T[keyof T]>>>()
// 获取事件监听器列表,如果不存在则创建
private getListeners<S extends keyof T>(name: S): Array<Listener<T[S]>> {
if (!this.events.has(name)) {
this.events.set(name, [])
}
return this.events.get(name) as Array<Listener<T[S]>>
}
// 按优先级排序监听器
private sortListeners<S extends keyof T>(name: S) {
const listeners = this.getListeners(name)
listeners.sort((a, b) => b.priority - a.priority)
}
// 打印事件和监听器信息
print() {
console.log("Registered Events:")
this.events.forEach((listeners, name) => {
// 显式处理 symbol 类型
const keyType = typeof name === "symbol" ? `Symbol(${name.description || ""})` : String(name)
console.log(` ${keyType}: ${listeners.length} listeners`)
})
}
// 添加事件监听器,支持优先级
on<S extends keyof T>(name: S, fn: T[S], priority = 0): this {
const listeners = this.getListeners(name)
listeners.push({ fn, once: false, priority })
this.sortListeners(name)
return this // 支持链式调用
}
// 触发事件
emit<S extends keyof T>(name: S, ...args: Parameters<T[S]>): this {
const listeners = this.getListeners(name).slice() // 创建副本以避免移除时的问题
for (const { fn } of listeners) {
try {
fn(...args)
} catch (error) {
console.error(`Error in event handler for ${String(name)}:`, error)
}
}
// 移除一次性监听器
if (listeners.some(l => l.once)) {
this.events.set(
name,
this.getListeners(name).filter(l => !l.once),
)
}
return this
}
// 移除事件监听器
off<S extends keyof T>(name: S, fn?: T[S]): this {
if (!this.events.has(name)) return this
const listeners = this.getListeners(name)
if (!fn) {
// 移除所有监听器
this.events.delete(name)
} else {
// 移除特定监听器
const filtered = listeners.filter(l => l.fn !== fn)
if (filtered.length === 0) {
this.events.delete(name)
} else {
this.events.set(name, filtered)
}
}
return this
}
// 添加一次性事件监听器
once<S extends keyof T>(name: S, fn: T[S], priority = 0): this {
const listeners = this.getListeners(name)
listeners.push({ fn, once: true, priority })
this.sortListeners(name)
return this
}
// 清除所有事件监听器
clear(): this {
this.events.clear()
return this
}
// 获取指定事件的监听器数量
listenerCount<S extends keyof T>(name: S): number {
return this.events.get(name)?.length || 0
}
// 检查事件是否有监听器
hasListeners<S extends keyof T>(name: S): boolean {
return this.listenerCount(name) > 0
}
// 获取所有事件名称
eventNames(): Array<keyof T> {
return Array.from(this.events.keys())
}
}
export function buildEmitter<T extends Record<string | symbol, FireFN>>() {
return new FireEvent<T>()
}

51
packages/base/event/main/index.ts

@ -0,0 +1,51 @@
// type FireKey = string
type FireFN = (...argu: any[]) => void
class FireEvent<T extends Record<string | symbol, FireFN>> {
#events: Record<keyof T, FireFN[]> = {} as any
print() {
Object.keys(this.#events).forEach(key => {
console.log(`${key}: ${this.#events[key]}\n`)
})
}
on<S extends keyof T>(name: S, fn: T[S]) {
if (!this.#events[name]) {
this.#events[name] = []
}
this.#events[name].push(fn)
}
emit<S extends keyof T>(name: S, ...argu: Parameters<T[S]>) {
if (this.#events[name]) {
this.#events[name].forEach(fn => {
fn(...argu)
})
}
}
off<S extends keyof T>(name: S, fn?: T[S]) {
const len = this.#events[name].length
if (!len) {
return
}
if (!fn) {
this.#events[name] = []
} else {
for (let i = len - 1; i >= 0; i--) {
const _fn = this.#events[name][i]
if (_fn === fn) {
this.#events[name].splice(i, 1)
}
}
}
}
once<S extends keyof T>(name: S, fn: T[S]) {
const _fn: any = (...argu: any[]) => {
fn(...argu)
this.off<S>(name, _fn)
}
this.on(name, _fn)
}
}
export function buildEmitter<T extends Record<string | symbol, FireFN>>() {
return new FireEvent<T>()
}

27
packages/base/index.ts

@ -0,0 +1,27 @@
// 抽象基类,使用泛型来正确推导子类类型
abstract class BaseSingleton {
private static _instance: any
public constructor() {
if (this.constructor === BaseSingleton) {
throw new Error("禁止直接实例化 BaseOne 抽象类")
}
if ((this.constructor as any)._instance) {
throw new Error("构造函数私有化失败,禁止重复 new")
}
// this.constructor 是子类,所以这里设为 instance
;(this.constructor as any)._instance = this
}
public static getInstance<T extends BaseSingleton>(this: new () => T): T {
const clazz = this as any as typeof BaseSingleton
if (!clazz._instance) {
clazz._instance = new this()
}
return clazz._instance as T
}
}
export { BaseSingleton }

13
packages/base/package.json

@ -0,0 +1,13 @@
{
"name": "base",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.4.1"
}

13
packages/helper/package.json

@ -0,0 +1,13 @@
{
"name": "helper",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.4.1"
}

7
packages/helper/updater/common.ts

@ -0,0 +1,7 @@
export const enum EventEnum {
UPDATE_PROGRESS = "update-progress",
}
export type EventMaps = {
[EventEnum.UPDATE_PROGRESS]: () => void
}

8
packages/helper/updater/main/handler.ts

@ -0,0 +1,8 @@
import { broadcast } from "main/utils"
import { EventEnum } from "../common"
export { EventEnum }
export function emit(key: EventEnum, ...args: any[]) {
broadcast(key, ...args)
}

74
packages/helper/updater/main/hot/download.ts

@ -0,0 +1,74 @@
type DownloadPercent = {
url: string
option?: object
onprocess?: (now: number, all: number) => void
onsuccess?: (data: any) => void
onerror?: (res: Response) => void
}
const RequestPercent = async ({
url = "",
option = {
headers: {
responseType: "arraybuffer",
},
},
onsuccess,
onerror,
onprocess,
}: DownloadPercent) => {
const response = (await fetch(url, option)) as any
if (!response.ok) {
onerror?.(response)
throw new Error(`下载失败`)
}
const reader = response?.body.getReader()
// 文件总长度
const contentLength = +response.headers.get("content-length")
let receivedLength = 0
const chunks: any[] = []
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
chunks.push(value)
receivedLength += value.length
onprocess?.(receivedLength, contentLength)
}
// 这里的chunksAll 已经是ArrayBuffer的数据类型了,可以直接返回,也可以转为blob处理
const chunksAll = new Uint8Array(receivedLength)
let position = 0
for (const chunk of chunks) {
chunksAll.set(chunk, position)
position += chunk.length
}
onsuccess?.(chunksAll)
return chunksAll
}
export { RequestPercent }
export default RequestPercent
// RequestPercent({
// url: "http://117.21.250.136:9812/ZxqyGateway/biz/file/downApk/%E6%98%93%E4%BC%81%E6%95%B0%E8%BD%AC%E5%B9%B3%E5%8F%B0app-1.2.7.apk",
// option: {
// headers: {
// responseType: "arraybuffer",
// },
// },
// onerror: () => {},
// onsuccess: data => {
// fs.writeFileSync("./aaa.apk", Buffer.from(data))
// console.log("success", data)
// },
// onprocess: (receivedLength, contentLength) => {
// console.log(receivedLength, contentLength)
// },
// })

118
packages/helper/updater/main/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 download from "./download"
import extract from "extract-zip"
import _logger from "logger/main"
import { emit, EventEnum } from "../handler"
const logger = _logger.createNamespace("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) {
if (isReadyUpdate) return
// 清除临时目录
clearUpdateTempDir()
// 创建临时目录
if (!fs.existsSync(updateTempDirPath)) {
fs.mkdirSync(updateTempDirPath, { recursive: true })
}
// 下载文件的本地保存路径
const downloadPath = path.join(updateTempDirPath, "update.zip")
try {
// 使用 fetch 下载更新包
const arrayBuffer = await download({
url: updatePackageUrl,
onprocess(now, all) {
logger.debug(`下载进度: ${((now / all) * 100).toFixed(2)}%`)
emit(EventEnum.UPDATE_PROGRESS, { percent: (now / all) * 100, now, all })
},
})
fs.writeFileSync(downloadPath, Buffer.from(arrayBuffer))
// 解压更新包
await extract(downloadPath, { dir: updateTempDirPath })
// 删除下载的zip文件
fs.unlinkSync(downloadPath)
isReadyUpdate = true
} catch (error) {
logger.debug("热更新包下载失败:", error)
throw error
}
}
function clearUpdateTempDir() {
if (!fs.existsSync(updateTempDirPath)) return
fs.rmSync(updateTempDirPath, { recursive: true })
}
export function flagNeedUpdate() {
shouldPerformHotUpdate = true
}

137
packages/helper/updater/main/index.ts

@ -0,0 +1,137 @@
import pkg from "electron-updater"
import { app, dialog } from "electron"
import Setting from "setting/main"
import EventEmitter from "events"
import { BaseSingleton } from "base"
import { fetchHotUpdatePackage, flagNeedUpdate } from "./hot"
import Locales from "locales/main"
import _logger from "logger/main"
const logger = _logger.createNamespace("updater")
const { autoUpdater } = pkg
class _Updater extends BaseSingleton {
public events = new EventEmitter()
private timer: ReturnType<typeof setInterval> | null = null
// autoReplace = false
async triggerHotUpdate(autoReplace = false) {
const url = Setting.values("update.hoturl")
await fetchHotUpdatePackage(url)
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()
}
}
constructor() {
super()
// 配置自动更新
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
// 检查更新错误
autoUpdater.on("error", error => {
logger.debug("Update error:", error)
})
// 检查更新
autoUpdater.on("checking-for-update", () => {
logger.debug("Checking for updates...")
})
// 有可用更新
autoUpdater.on("update-available", info => {
logger.debug("Update available:", info)
this.promptUserToUpdate()
})
// 没有可用更新
autoUpdater.on("update-not-available", info => {
logger.debug("Update not available:", info)
})
// 更新下载进度
autoUpdater.on("download-progress", progressObj => {
logger.debug(
`Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`,
)
})
// 更新下载完成
autoUpdater.on("update-downloaded", info => {
logger.debug("Update downloaded:", info)
this.promptUserToInstall()
})
}
init() {
// 定期检查更新
this.checkForUpdates()
this.timer && clearInterval(this.timer)
this.timer = setInterval(
() => {
this.checkForUpdates()
},
1000 * 60 * 60,
) // 每小时检查一次
}
destroy() {
// 清理工作
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
private async checkForUpdates() {
if (app.isPackaged) {
try {
await autoUpdater.checkForUpdates()
logger.debug("Updater初始化检查成功.")
} catch (error) {
logger.debug("Failed to check for updates:", error)
}
} else {
logger.debug("正在开发模式,跳过更新检查.")
}
}
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)
}
}
}
const Updater = _Updater.getInstance()
export { Updater }
export default Updater

0
packages/helper/updater/renderer.ts

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 config from "config"
import * as rfs from "rotating-file-stream"
import { LogLevel, LogLevelColor, LogLevelName } from "./common"
import { emitter } from "setting/main/event"
// 重置颜色的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: config.default_config.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()
emitter.on("update", setting => {
logger.setLevel(setting.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
* })
*/

231
packages/setting/main.ts

@ -0,0 +1,231 @@
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 _logger from "logger/main"
const logger = _logger.createNamespace("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() {
logger.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() {
logger.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 }
export type { IConfig, IOnFunc }

6
packages/setting/main/event.ts

@ -0,0 +1,6 @@
import { buildEmitter } from "base/event/main"
import type { IOnFunc } from "setting/main"
export const emitter = buildEmitter<{
update: IOnFunc
}>()

7
packages/setting/package.json

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

928
pnpm-lock.yaml

File diff suppressed because it is too large

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>

34
src/common/_ioc.main.ts

@ -0,0 +1,34 @@
import { Container, ContainerModule } from "inversify"
/**
*
*/
const commandModules = import.meta.glob("./event/**/main/command.{ts,js}", { eager: true })
const modules = new ContainerModule(bind => {
// 自动绑定所有命令类
Object.values(commandModules).forEach(module => {
// 由于 module 类型为 unknown,先进行类型断言为包含 default 属性的对象
const CommandClass = (module as { default: any }).default
if (CommandClass) {
const className = CommandClass.name.replace("Command", "")
if (CommandClass["init"]) {
CommandClass["init"]()
}
bind(className + "Command")
.to(CommandClass)
.inSingletonScope()
}
})
})
/**
*
* @param ioc - Inversify
*/
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>()
}

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

@ -0,0 +1,58 @@
import { ApiFactory } from "common/lib/abstract"
import { BaseSingleton } from "base"
import { LogLevel } from "packages/logger/common"
class PlatForm extends BaseSingleton {
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 showSrd() {
// return this.api.call("BasicService.showAbout")
return this.api.call("PlatFormCommand.showSrd")
}
async getSrdCookie() {
// return this.api.call("BasicService.showAbout")
return this.api.call("PlatFormCommand.getSrdCookie")
}
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 }

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

@ -0,0 +1,121 @@
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,
title: "关于我",
show: true,
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()
}
}

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

@ -0,0 +1,5 @@
import { Snippet } from "."
export function useSnippet() {
return Snippet.getInstance()
}

18
src/common/event/Snippet/index.ts

@ -0,0 +1,18 @@
import { BaseSingleton } from "base"
import { ApiFactory } from "common/lib/abstract"
class Snippet extends BaseSingleton {
constructor() {
super()
}
private get api() {
return ApiFactory.getApiClient()
}
getTree = async () => {
return this.api.call("SnippetCommand.getTree")
}
}
export { Snippet }

44
src/common/event/Snippet/main/command.ts

@ -0,0 +1,44 @@
import fs from "fs-extra"
import path from "path/posix"
import Setting from "setting/main"
// 代码片段命令处理器
// base/__snippet__.json 基础信息
// 路径做为ID, 当前文件夹的信息
export default class SnippetCommand {
storagePath: string = Setting.values("snippet.storagePath")
constructor() {
const handler = {
get: function (target, prop, receiver) {
if (!target["check"]()) {
throw new Error(`代码片段路径存在问题`)
}
const value = target[prop]
if (typeof value === "function") {
return (...args) => Reflect.apply(value, receiver, args)
}
return value
},
}
return new Proxy(this, handler)
}
async check() {
const stat = await fs.statSync(this.storagePath)
const inforFile = path.resolve(this.storagePath, "__snippet__.json")
if (stat.isDirectory() && stat.size == 0) {
await fs.writeJSON(inforFile, {})
// 空文件夹, 初始化信息
return true
} else {
const isExist = await fs.pathExists(inforFile)
return isExist
}
}
getTree() {
return this.storagePath
}
}

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

@ -0,0 +1,54 @@
import { BaseSingleton } from "base"
export class Tabs extends BaseSingleton {
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

15
src/common/event/Updater/index.ts

@ -0,0 +1,15 @@
import { EventEnum } from "helper/updater/common"
const curProgress = ref(0)
api.on(EventEnum.UPDATE_PROGRESS, ({ percent, now, all }) => {
curProgress.value = percent
})
function useUpdate() {
return {
curProgress,
}
}
export { useUpdate }

15
src/common/event/Updater/main/command.ts

@ -0,0 +1,15 @@
import Updater from "helper/updater/main"
import _logger from "logger/main"
const logger = _logger.createNamespace("UpdaterCommand")
export default class UpdaterCommand {
static init() {
// 命令初始化
logger.debug("UpdaterCommand init")
}
async triggerHotUpdate() {
Updater.triggerHotUpdate()
}
}

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

7
src/common/readme.md

@ -0,0 +1,7 @@
## event
通用事件处理模块
- main/**/* 处理主进程的模块
- main/command.ts 会通过ioc收集,进入依赖管理中
- 其他 处理渲染进程的模块

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

131
src/main/App.ts

@ -3,87 +3,88 @@ 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"
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,
) {
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() {
// 新开窗口的时候,会有个窗口闪烁的问题,也可以理解为渐入效果
// 主进程中添加如下代码即可
app.commandLine.appendSwitch("wm-window-animations-disabled")
// 开启硬件加速
app.disableHardwareAcceleration()
crashHandler.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 }

130
src/main/base/LinkedList.ts

@ -0,0 +1,130 @@
class Node<E> {
static readonly Undefined = new Node<any>(undefined)
element: E
next: Node<E>
prev: Node<E>
constructor(element: E) {
this.element = element
this.next = Node.Undefined
this.prev = Node.Undefined
}
}
export class LinkedList<E> {
private _first: Node<E> = Node.Undefined
private _last: Node<E> = Node.Undefined
private _size: number = 0
get size(): number {
return this._size
}
isEmpty(): boolean {
return this._first === Node.Undefined
}
clear(): void {
let node = this._first
while (node !== Node.Undefined) {
const next = node.next
node.prev = Node.Undefined
node.next = Node.Undefined
node = next
}
this._first = Node.Undefined
this._last = Node.Undefined
this._size = 0
}
unshift(element: E): () => void {
return this._insert(element, false)
}
push(element: E): () => void {
return this._insert(element, true)
}
private _insert(element: E, atTheEnd: boolean): () => void {
const newNode = new Node(element)
if (this._first === Node.Undefined) {
this._first = newNode
this._last = newNode
} else if (atTheEnd) {
// push
const oldLast = this._last
this._last = newNode
newNode.prev = oldLast
oldLast.next = newNode
} else {
// unshift
const oldFirst = this._first
this._first = newNode
newNode.next = oldFirst
oldFirst.prev = newNode
}
this._size += 1
let didRemove = false
return () => {
if (!didRemove) {
didRemove = true
this._remove(newNode)
}
}
}
shift(): E | undefined {
if (this._first === Node.Undefined) {
return undefined
} else {
const res = this._first.element
this._remove(this._first)
return res
}
}
pop(): E | undefined {
if (this._last === Node.Undefined) {
return undefined
} else {
const res = this._last.element
this._remove(this._last)
return res
}
}
private _remove(node: Node<E>): void {
if (node.prev !== Node.Undefined && node.next !== Node.Undefined) {
// middle
const anchor = node.prev
anchor.next = node.next
node.next.prev = anchor
} else if (node.prev === Node.Undefined && node.next === Node.Undefined) {
// only node
this._first = Node.Undefined
this._last = Node.Undefined
} else if (node.next === Node.Undefined) {
// last
this._last = this._last.prev!
this._last.next = Node.Undefined
} else if (node.prev === Node.Undefined) {
// first
this._first = this._first.next!
this._first.prev = Node.Undefined
}
// done
this._size -= 1
}
*[Symbol.iterator](): Iterator<E> {
let node = this._first
while (node !== Node.Undefined) {
yield node.element
node = node.next
}
}
}

56
src/main/base/base.ts

@ -1,9 +1,57 @@
import EventEmitter from "node:events"
// import EventEmitter from "node:events"
// import { LinkedList } from "./LinkedList"
// abstract class BaseClass {
// constructor() {}
// public _events = new EventEmitter()
// private _event: Function | undefined
// private _listeners: LinkedList<any> | undefined
// abstract fire(event: string, ...args: any[])
// // 允许大家订阅此发射器的事件
// get event() {
// if (!this._event) {
// this._event = (listener, thisArgs?: any, disposables?) => {
// if (!this._listeners) {
// this._listeners = new LinkedList()
// }
// // 往队列中添加该 Listener,同时返回一个移除该 Listener 的方法
// const remove = this._listeners.push(!thisArgs ? listener : [listener, thisArgs])
// let result
// // 返回一个带 dispose 方法的结果,dispose 执行时会移除该 Listener
// result = {
// dispose: () => {
// result.dispose = Emitter._noop
// if (!this._disposed) {
// remove()
// }
// },
// }
// if (disposables instanceof DisposableStore) {
// disposables.add(result)
// } else if (Array.isArray(disposables)) {
// disposables.push(result)
// }
// return result
// }
// }
// return
// }
// dispose() {}
// abstract init(...argus: any[])
// abstract destroy()
// }
// export { BaseClass }
// export default BaseClass
abstract class BaseClass {
public _events = new EventEmitter()
abstract init(...argus: any[])
abstract destroy()
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 }

24
src/main/controller/_ioc.ts

@ -1,14 +1,28 @@
import { Container, ContainerModule } from "inversify"
import BasicService from "./BasicService"
import TabsService from "./TabsService"
/**
*
*/
const serviceModules = import.meta.glob("./*.{ts,js}", { eager: true })
const modules = new ContainerModule(bind => {
bind("BasicService").to(BasicService).inSingletonScope()
bind("TabsService").to(TabsService).inSingletonScope()
// 自动绑定所有服务类
Object.values(serviceModules).forEach(module => {
// 由于 module 类型为 unknown,需要进行类型断言
const ServiceClass = (module as { default: any }).default
if (ServiceClass && ServiceClass.name.endsWith("Service")) {
const serviceName = ServiceClass.name
bind(serviceName).to(ServiceClass).inSingletonScope()
}
})
})
/**
*
* @param ioc - Inversify
*/
async function destroyAllController(ioc: Container) {
await ioc.unloadAsync(modules)
await ioc.unloadAsync(modules)
}
export { modules, destroyAllController }

60
src/main/debug.ts

@ -0,0 +1,60 @@
import debug from "debug"
import { app } from "electron"
import path from "node:path"
import logger from "logger/main"
import * as rfs from "rotating-file-stream"
import fs from "fs"
// 配置根目录
const logsPath = app.getPath("logs")
logger.debug(`日志地址:${logsPath}`)
const LOG_ROOT = path.join(logsPath)
// 缓存当前应用启动的日志文件流
let currentLogStream: rfs.RotatingFileStream | null = null
// 生成当前启动时的日志文件名
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)
// 确保日志目录存在
if (!fs.existsSync(LOG_ROOT)) {
fs.mkdirSync(LOG_ROOT, { recursive: true })
}
// 延迟初始化日志流,直到第一次写入
if (!currentLogStream) {
const logFileName = getLogFileName()
currentLogStream = rfs.createStream(logFileName, {
path: LOG_ROOT,
size: "10M", // 单个文件最大 10MB
rotate: 10, // 保留最近 10 个文件
})
}
// @ts-ignore 获取当前命名空间
const namespace = this.namespace || "unknown"
// 写入日志(添加时间戳和命名空间)
const timestamp = new Date().toISOString()
const message = args.join(" ")
currentLogStream.write(`[${timestamp}] [${namespace}] ${message}\n`)
}
app.on("before-quit", () => {
if (currentLogStream) {
currentLogStream.end()
currentLogStream.destroy()
currentLogStream = null
}
})

6
src/main/event.ts

@ -0,0 +1,6 @@
import EventEmitter from "events"
const globalEvent = new EventEmitter()
export default globalEvent
export { globalEvent as eventbus }

84
src/main/index.ts

@ -1,85 +1,11 @@
import "reflect-metadata"
import "./debug"
import "setting/main"
import "logger/main"
import "logger/main-error"
import { _ioc } from "main/_ioc"
import { App } from "main/App"
import debug from "debug"
import fs from "fs"
import path from "path"
import * as rfs from "rotating-file-stream"
import { app } from "electron"
// 配置根目录
const logsPath = app.getPath("logs")
console.log(`日志地址:${logsPath}`)
const LOG_ROOT = path.join(logsPath)
// 缓存已创建的文件流(避免重复创建)
const streams = new Map()
// 转换命名空间为安全路径
function sanitizeNamespace(namespace) {
return namespace
.split(":") // 按层级分隔符拆分
.map(part => part.replace(/[\\/:*?"<>|]/g, "_")) // 替换非法字符
.join(path.sep) // 拼接为系统路径分隔符(如 / 或 \)
}
// 覆盖 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`)
// 确保目录存在
const dir = path.dirname(logFilePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { 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)
}
// 写入日志(添加时间戳)
const message = args.join(" ")
stream.write(`${message}\n`)
// const timestamp = new Date().toISOString()
// stream.write(`[${timestamp}] ${message}\n`)
}
const curApp = _ioc.get(App)
curApp.init()
const _debug = debug("app:app")
app.on("before-quit", () => {
_debug("应用关闭")
streams.forEach(stream => {
stream.end()
stream.destroy()
})
streams.clear()
})

36
src/main/modules/_ioc.ts

@ -1,36 +1,30 @@
import { Container, ContainerModule } from "inversify"
import { Setting } from "./setting"
import { DB } from "./db"
import { Api } from "./api"
import { WindowManager } from "./window-manager"
import { Tabs } from "./tabs"
import Commands from "./commands"
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(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(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 = ""
}

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

@ -1,81 +1,81 @@
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"
import _debug from "debug"
import _logger from "logger/main"
const debug = _debug("app:db")
const logger = _logger.createNamespace("db")
@injectable()
class DB extends BaseClass {
destroy() {
debug(`DB destroy`)
}
Modules: Record<string, CustomLow<any>> = {}
destroy() {
logger.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 }
}

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

@ -3,288 +3,286 @@ 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"
import EventEmitter from "node:events"
const debug = _debug("app:tab")
const debug = _debug("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
private defaultOptions: IOption = {
url: "",
active: false,
}
init() {
// TODO
}
public events = new EventEmitter()
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 options: IOption
private defaultOptions: IOption = {
url: "",
active: false,
}
get isActive() {
return this.active
}
private options: IOption
get events() {
return this._events
}
get isActive() {
return this.active
}
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)
}
destroyTimer: NodeJS.Timeout | null = null
stopDestroyTimer() {
if (this.destroyTimer !== null) {
clearTimeout(this.destroyTimer)
this.destroyTimer = null
}
constructor(options = {}, window: BrowserWindow, curRect?: IRect) {
super()
this.listenResize = this.listenResize.bind(this)
this.options = {
...this.defaultOptions,
...options,
}
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 }

206
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")
const debug = _debug("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 }

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

@ -1,114 +0,0 @@
import pkg from "electron-updater"
import { app, dialog } from "electron"
import { injectable } from "inversify"
import BaseClass from "main/base/base"
// import { Setting } from "../setting"
import _debug from "debug"
import EventEmitter from "events"
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()
})
}
init() {
// 定期检查更新
this.checkForUpdates()
setInterval(
() => {
this.checkForUpdates()
},
1000 * 60 * 60,
) // 每小时检查一次
}
destroy() {
// 清理工作
}
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 promptUserToInstall() {
const result = await dialog.showMessageBox({
type: "info",
title: "更新已就绪",
message: "新版本已下载完成,是否立即安装?",
buttons: ["立即安装", "稍后安装"],
defaultId: 0,
})
if (result.response === 0) {
autoUpdater.quitAndInstall(false, true)
}
}
}
export default Updater

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

@ -3,344 +3,369 @@ import { cloneDeep, merge } from "lodash"
import { defaultConfig, defaultWindowConfig, getWindowsMap, IConfig, Param } from "./windowsMap"
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>){
const 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,
},
},
},
}
},
},
}
}

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

@ -1,49 +1,475 @@
import { session, net } from "electron"
import { injectable } from "inversify"
import BaseClass from "main/base/base"
import _debug from "debug"
import _logger from "logger/main"
import fs from "fs"
import path from "path"
import { app } from "electron"
const debug = _debug("app:zephyr")
const logger = _logger.createNamespace("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()
logger.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) {
logger.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) {
logger.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()
}
init(partition?: string) {
const ses = partition ? session.fromPartition(partition) : session.defaultSession
ses.protocol.handle("zephyr", this.interceptHandlerZephyr)
logger.debug("zephyr initialized with partition:", partition)
}
setAllowedPaths(config: Partial<typeof this.pathConfig>) {
Object.assign(this.pathConfig, config)
logger.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)) {
logger.debug("不安全的路径字符:", filePath)
return false
}
// 2. 检查是否包含 .. 路径
if (filePath.includes("..")) {
logger.debug("检测到路径遍历尝试")
return false
}
// 3. 检查符号链接
if (await this.isSymlink(filePath)) {
logger.debug("不允许访问符号链接")
return false
}
// 4. 检查文件大小
if (!(await this.checkFileSize(filePath))) {
logger.debug("文件超出大小限制")
return false
}
// 5. 文件类型检查
const ext = path.extname(filePath).toLowerCase()
if (!this.ALLOWED_EXTENSIONS.includes(ext)) {
logger.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) {
logger.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)
logger.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))) {
logger.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) {
logger.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:
logger.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, "/")
}

47
src/main/utils/session/cookies.ts

@ -0,0 +1,47 @@
import { BrowserView, BrowserWindow } from "electron"
const cookies = {
getCurrCookies(params = {}, currWin: BrowserView | BrowserWindow) {
let currSession = currWin.webContents.session
return currSession.cookies.get(Object.assign({}, params))
},
removeCurrCookies(cookies = [], currWin: BrowserView | BrowserWindow) {
let currSession = currWin.webContents.session
let err = []
let apiCount = 0
return new Promise((resove, reject) => {
cookies.forEach(async (item: any) => {
await currSession.cookies.remove(`http://${item.domain}`, item.name)
apiCount = apiCount + 1
if (err.length === apiCount) {
resove({ message: "cookie 清除成功" })
} else {
reject(err)
}
})
})
},
setCurrCookies(cookies = [], currWin: BrowserView | BrowserWindow) {
let currSession = currWin.webContents.session
let err = []
let apiCount = 0
return new Promise((resove, reject) => {
cookies.forEach(async (item: any) => {
await currSession.cookies.set(
Object.assign({}, item, {
url: `http://${item.domain}`,
name: item.name,
}),
)
apiCount = apiCount + 1
if (err.length === apiCount) {
resove({ message: "cookie 设置成功!" })
} else {
reject(err)
}
})
})
},
}
export default cookies

82
src/main/utils/session/index.ts

@ -0,0 +1,82 @@
// https://blog.guowenfh.com/2017/10/21/2017/electron-multiple-session/#%E5%9C%A8-webview-%E4%B8%AD
import { BrowserWindow } from "electron"
/**
*
* session
* Promise {partition,userinfo,cookies}
* @returns Promise
*/
function createLoginWin(partition) {
partition = partition || `persist:${Math.random()}`
// const charset = require("superagent-charset")
// const request = charset(require("superagent")) // HTTP
let presWindow = new BrowserWindow({
width: 1280,
height: 768,
title: "用户登陆",
webPreferences: {
webSecurity: false,
allowRunningInsecureContent: true,
partition,
},
})
let webContents = presWindow.webContents
return new Promise(function (resove, _) {
// webContents.openDevTools();
presWindow.loadURL("https://login.taobao.com/member/login.jhtml")
webContents.on("did-navigate-in-page", async function () {
// 这里可以看情况进行参数的传递,获取制定的 cookies
const cookies = await webContents.session.cookies.get({})
let obj = { partition, cookies }
resove(obj)
// webContents.session.cookies.get({}, function (err, cookies) {
// if (err) {
// presWindow.close() // 关闭登陆窗口
// return reject(err)
// }
// let obj = { partition, cookies }
// resove(obj)
// fetch("https://login.taobao.com/member/login.jhtml", {
// method: "GET",
// credentials: "include",
// headers: {
// Cookie: cookies.map(item => `${item.name}=${item.value};`).join(" "),
// "Content-Type": "application/json",
// },
// })
// .then(response => response.json())
// .then(data => {
// console.log(data)
// presWindow.close()
// resove(obj)
// })
// .catch(err => {
// presWindow.close()
// reject(err)
// })
// })
// 这一步并不是必需的。
// request
// .get("http://taobao.com/userinfo")
// .query({ _: Date.now() }) // query string
// .set("Cookie", cookies.map(item => `${item.name}=${item.value};`).join(" "))
// .end(function (err, res) {
// presWindow.close()
// if (err) {
// return reject(err)
// }
// if (!res || !res.body || !res.body.result !== 1) {
// return reject(res.body)
// }
// let obj = { partition, cookies, userinfo: res.body.data }
// resove(obj)
// })
})
// })
})
}
export {
createLoginWin
}

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
}

0
src/preload/plugin.ts

54
src/renderer/about.html

@ -1,28 +1,32 @@
<!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>
<body>
<article>
<h1 id="demo" style="text-align: center">您好,亲爱的冒险者!</h1>
</article>
</body>
</html>
<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>环境</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']>

16
src/renderer/components.d.ts

@ -9,7 +9,21 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AdjustLine: typeof import('./src/components/AdjustLine.vue')['default']
NavBar: typeof import('./src/components/NavBar.vue')['default']
CodeEditor: typeof import('./src/components/CodeEditor/code-editor.vue')['default']
IconBiCollectionFill: typeof import('~icons/bi/collection-fill')['default']
IconBiTrashFill: typeof import('~icons/bi/trash-fill')['default']
IconFluentCollections24Regular: typeof import('~icons/fluent/collections24-regular')['default']
IconFluentCollectionsEmpty16Filled: typeof import('~icons/fluent/collections-empty16-filled')['default']
IconHugeiconsInbox: typeof import('~icons/hugeicons/inbox')['default']
IconIconParkSolidCollectComputer: typeof import('~icons/icon-park-solid/collect-computer')['default']
IconIconParkTwotoneDataAll: typeof import('~icons/icon-park-twotone/data-all')['default']
IconMdiLightInbox: typeof import('~icons/mdi-light/inbox')['default']
IconMynauiTrash: typeof import('~icons/mynaui/trash')['default']
IconSolarBoxBoldDuotone: typeof import('~icons/solar/box-bold-duotone')['default']
IconSolarHome2Outline: typeof import('~icons/solar/home2-outline')['default']
'IconStash:arrowReplyDuotone': typeof import('~icons/stash/arrow-reply-duotone')['default']
IconTdesignMapCollection: typeof import('~icons/tdesign/map-collection')['default']
NavBar: typeof import('./src/ui/NavBar.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Versions: typeof import('./src/components/Versions.vue')['default']

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>

35
src/renderer/src/App.vue

@ -1,9 +1,32 @@
<script setup lang="ts"></script>
<script setup lang="ts">
</script>
<template>
<router-view v-slot="{ Component, route }">
<transition name="slide">
<component :is="Component" :key="route" />
</transition>
</router-view>
<div h-full flex flex-col overflow-hidden>
<NavBar></NavBar>
<div flex-1 h-0 overflow-hidden flex flex-col relative id="page-container" style="transform: scale(1);">
<router-view v-slot="{ Component, route }">
<Transition name="slide-fade" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</Transition>
</router-view>
</div>
</div>
</template>
<style lang="scss" scoped>
.slide-fade-enter-active {
transition: all 0.2s ease-out;
}
.slide-fade-leave-active {
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
// transform: translateX(20px);
opacity: 0;
}
</style>

427
src/renderer/src/assets/libs/scrollbot.ts

@ -1,246 +1,245 @@
interface ScrollStyle {
[key: string]: string
[key: string]: string
}
class Scrollbot {
private orgPar!: HTMLElement
private sbw: number = 5
private scrollSpeed: number = 200
private parContent!: string
private newPar!: HTMLDivElement
// private sbContainer!: HTMLDivElement
private scrollBarHolder!: HTMLDivElement
private scrollBar!: HTMLDivElement
private inP!: HTMLDivElement
private sbHeight: number = 0
private mdown: boolean = false
private customHeight: boolean = false
// private scrollElement!: HTMLElement
private onScrollF?: () => void
private sB: ScrollStyle = {}
private sBH: ScrollStyle = {}
private posCorrection: number = 0
private btmCorrection: number = 0
private relY: number = 0
private pC: number = 0
getDom(selector: string | HTMLElement) {
if (typeof selector === "string") {
return document.querySelector<HTMLElement>(selector)
}
return selector
private orgPar!: HTMLElement
private sbw: number = 5
private scrollSpeed: number = 200
private parContent!: string
private newPar!: HTMLDivElement
// private sbContainer!: HTMLDivElement
private scrollBarHolder!: HTMLDivElement
private scrollBar!: HTMLDivElement
private inP!: HTMLDivElement
private sbHeight: number = 0
private mdown: boolean = false
private customHeight: boolean = false
// private scrollElement!: HTMLElement
private onScrollF?: () => void
private sB: ScrollStyle = {}
private sBH: ScrollStyle = {}
private posCorrection: number = 0
private btmCorrection: number = 0
private relY: number = 0
private pC: number = 0
getDom(selector: string | HTMLElement) {
if (typeof selector === "string") {
return document.querySelector<HTMLElement>(selector)
}
return selector
}
constructor(selector: string | HTMLElement, width?: number) {
const element = this.getDom(selector)
if (!element) throw new Error("Element not found")
this.orgPar = element
constructor(selector: string | HTMLElement, width?: number) {
const element = this.getDom(selector)
if (!element) throw new Error("Element not found")
this.orgPar = element
const ieVersion = this.isIE()
if (!ieVersion || (ieVersion && ieVersion < 9)) {
this.init(width)
}
const ieVersion = this.isIE()
if (!ieVersion || (ieVersion && ieVersion < 9)) {
this.init(width)
}
private init(width?: number): void {
this.sbw = width ?? 5
this.parContent = this.orgPar.innerHTML
this.orgPar.innerHTML = ""
this.setupElements()
this.setupStyles()
this.setupEventListeners()
this.refresh()
}
private init(width?: number): void {
this.sbw = width ?? 5
this.parContent = this.orgPar.innerHTML
this.orgPar.innerHTML = ""
this.setupElements()
this.setupStyles()
this.setupEventListeners()
this.refresh()
}
private setupElements(): void {
this.newPar = document.createElement("div")
// this.sbContainer = document.createElement("div")
this.scrollBarHolder = document.createElement("div")
this.scrollBar = document.createElement("div")
this.inP = document.createElement("div")
this.newPar.className = "scrollbot-outer-parent"
this.scrollBarHolder.className = "scrollbot-scrollbar-holder"
this.scrollBar.className = "scrollbot-scrollbar"
this.inP.className = "scrollbot-inner-parent"
this.inP.innerHTML = this.parContent
this.newPar.appendChild(this.inP)
this.scrollBarHolder.appendChild(this.scrollBar)
this.newPar.appendChild(this.scrollBarHolder)
this.orgPar.appendChild(this.newPar)
}
private setupStyles(): void {
this.newPar.style.position = "relative"
this.newPar.style.paddingRight = `${this.sbw}px`
this.newPar.style.zIndex = "9999999"
this.newPar.style.height = "100%"
this.newPar.style.overflow = "hidden"
this.inP.style.cssText = `height:100%;overflow-y:auto;overflow-x:hidden;padding-right:${
this.sbw + 20
}px;width:100%;box-sizing:content-box;`
this.sbHeight = (this.inP.clientHeight * 100) / this.inP.scrollHeight
// this.scrollElement = this.inP
this.updateScrollbarStyles()
}
private updateScrollbarStyles(): void {
this.sB = {
width: `${this.sbw}px`,
height: `${this.sbHeight}%`,
position: "absolute",
right: "0",
top: "0",
backgroundColor: "#444444",
borderRadius: "15px",
}
private setupElements(): void {
this.newPar = document.createElement("div")
// this.sbContainer = document.createElement("div")
this.scrollBarHolder = document.createElement("div")
this.scrollBar = document.createElement("div")
this.inP = document.createElement("div")
this.newPar.className = "scrollbot-outer-parent"
this.scrollBarHolder.className = "scrollbot-scrollbar-holder"
this.scrollBar.className = "scrollbot-scrollbar"
this.inP.className = "scrollbot-inner-parent"
this.inP.innerHTML = this.parContent
this.newPar.appendChild(this.inP)
this.scrollBarHolder.appendChild(this.scrollBar)
this.newPar.appendChild(this.scrollBarHolder)
this.orgPar.appendChild(this.newPar)
}
private setupStyles(): void {
this.newPar.style.position = "relative"
this.newPar.style.paddingRight = `${this.sbw}px`
this.newPar.style.zIndex = "9999999"
this.newPar.style.height = "100%"
this.newPar.style.overflow = "hidden"
this.inP.style.cssText = `height:100%;overflow-y:auto;overflow-x:hidden;padding-right:${
this.sbw + 20
}px;width:100%;box-sizing:content-box;`
this.sbHeight = (this.inP.clientHeight * 100) / this.inP.scrollHeight
// this.scrollElement = this.inP
this.updateScrollbarStyles()
this.sBH = {
width: `${this.sbw}px`,
height: "100%",
position: "absolute",
right: "0",
top: "0",
backgroundColor: "#ADADAD",
borderRadius: "15px",
}
private updateScrollbarStyles(): void {
this.sB = {
width: `${this.sbw}px`,
height: `${this.sbHeight}%`,
position: "absolute",
right: "0",
top: "0",
backgroundColor: "#444444",
borderRadius: "15px",
}
Object.assign(this.scrollBar.style, this.sB)
Object.assign(this.scrollBarHolder.style, this.sBH)
}
this.sBH = {
width: `${this.sbw}px`,
height: "100%",
position: "absolute",
right: "0",
top: "0",
backgroundColor: "#ADADAD",
borderRadius: "15px",
}
public refresh(): void {
this.sbHeight = (this.inP.clientHeight * 100) / this.inP.scrollHeight
this.scrollBarHolder.style.display = this.sbHeight >= 100 ? "none" : "block"
Object.assign(this.scrollBar.style, this.sB)
Object.assign(this.scrollBarHolder.style, this.sBH)
if (this.inP.scrollHeight > this.inP.clientHeight) {
this.scrollBar.style.height = this.customHeight ? this.sB.height : `${this.sbHeight}%`
}
public refresh(): void {
this.sbHeight = (this.inP.clientHeight * 100) / this.inP.scrollHeight
this.scrollBarHolder.style.display = this.sbHeight >= 100 ? "none" : "block"
if (this.inP.scrollHeight > this.inP.clientHeight) {
this.scrollBar.style.height = this.customHeight ? this.sB.height : `${this.sbHeight}%`
}
}
public destroy(): void {
this.orgPar.innerHTML = this.parContent
this.orgPar.style.overflow = "auto"
}
private isIE(): number | false {
const userAgent = navigator.userAgent.toLowerCase()
const msie = userAgent.indexOf("msie")
return msie !== -1 ? parseInt(userAgent.split("msie")[1]) : false
}
public onScroll(callback: () => void): void {
this.onScrollF = callback
}
private setupEventListeners(): void {
this.setupScrollListener()
this.setupMouseEvents()
}
private setupScrollListener(): void {
this.inP.addEventListener("scroll", () => {
const scrollPercentage = (this.inP.scrollTop * 100) / this.inP.scrollHeight
const correction =
((this.sbHeight - parseFloat(this.sB.height)) * this.inP.scrollTop) / (this.inP.scrollHeight - this.inP.clientHeight)
this.scrollBar.style.top = `${scrollPercentage + correction}%`
if (this.onScrollF) {
this.onScrollF()
}
})
}
private setScroll(position: number, duration: number = 500): void {
if (position >= this.inP.scrollHeight - this.inP.clientHeight) {
position = this.inP.scrollHeight - this.inP.clientHeight
}
public destroy(): void {
this.orgPar.innerHTML = this.parContent
this.orgPar.style.overflow = "auto"
const difference = position - this.inP.scrollTop
const perTick = (difference / duration) * 10
setTimeout(() => {
this.inP.scrollTop += perTick
if (Math.abs(position - this.inP.scrollTop) < 5) return
this.setScroll(position, duration - 10)
}, 10)
}
private setupMouseEvents(): void {
// 滚动条容器点击事件
this.scrollBarHolder.onmousedown = (e: MouseEvent) => {
if (e.target !== this.scrollBarHolder) return
const relPos = ((e.pageY - this.scrollBarHolder.getBoundingClientRect().top) * 100) / this.scrollBarHolder.clientHeight
this.setScroll((this.inP.scrollHeight * relPos) / 100, this.scrollSpeed)
}
private isIE(): number | false {
const userAgent = navigator.userAgent.toLowerCase()
const msie = userAgent.indexOf("msie")
return msie !== -1 ? parseInt(userAgent.split("msie")[1]) : false
// 滚动条拖动事件
this.scrollBar.onmousedown = (e: MouseEvent) => {
this.mdown = true
this.posCorrection = e.pageY - this.scrollBar.getBoundingClientRect().top
this.btmCorrection = (this.scrollBar.clientHeight * 100) / this.newPar.clientHeight
return false
}
public onScroll(callback: () => void): void {
this.onScrollF = callback
// 全局鼠标事件
document.onmouseup = () => {
this.mdown = false
}
private setupEventListeners(): void {
this.setupScrollListener()
this.setupMouseEvents()
}
private setupScrollListener(): void {
this.inP.addEventListener("scroll", () => {
const scrollPercentage = (this.inP.scrollTop * 100) / this.inP.scrollHeight
const correction =
((this.sbHeight - parseFloat(this.sB.height)) * this.inP.scrollTop) / (this.inP.scrollHeight - this.inP.clientHeight)
this.scrollBar.style.top = `${scrollPercentage + correction}%`
if (this.onScrollF) {
this.onScrollF()
}
})
}
private setScroll(position: number, duration: number = 500): void {
if (position >= this.inP.scrollHeight - this.inP.clientHeight) {
position = this.inP.scrollHeight - this.inP.clientHeight
}
const difference = position - this.inP.scrollTop
const perTick = (difference / duration) * 10
setTimeout(() => {
this.inP.scrollTop += perTick
if (Math.abs(position - this.inP.scrollTop) < 5) return
this.setScroll(position, duration - 10)
}, 10)
}
private setupMouseEvents(): void {
// 滚动条容器点击事件
this.scrollBarHolder.onmousedown = (e: MouseEvent) => {
if (e.target !== this.scrollBarHolder) return
const relPos = ((e.pageY - this.scrollBarHolder.getBoundingClientRect().top) * 100) / this.scrollBarHolder.clientHeight
this.setScroll((this.inP.scrollHeight * relPos) / 100, this.scrollSpeed)
}
// 滚动条拖动事件
this.scrollBar.onmousedown = (e: MouseEvent) => {
this.mdown = true
this.posCorrection = e.pageY - this.scrollBar.getBoundingClientRect().top
this.btmCorrection = (this.scrollBar.clientHeight * 100) / this.newPar.clientHeight
return false
document.onmousemove = (e: MouseEvent) => {
if (this.mdown) {
// 清除文本选择
window.getSelection()?.removeAllRanges()
this.relY = e.pageY - this.newPar.getBoundingClientRect().top
this.pC = ((this.relY - this.posCorrection) * 100) / this.newPar.clientHeight
if (this.pC >= 0 && this.pC + this.btmCorrection <= 100) {
this.scrollBar.style.top = `${this.pC}%`
this.inP.scrollTop =
((parseFloat(this.scrollBar.style.top) -
((this.sbHeight - parseFloat(this.sB.height)) * this.inP.scrollTop) / (this.inP.scrollHeight - this.inP.clientHeight)) *
this.inP.scrollHeight) /
100
} else if (this.pC < 0 && parseFloat(this.scrollBar.style.top) > 0) {
this.scrollBar.style.top = "0%"
this.inP.scrollTop = 0
}
// 全局鼠标事件
document.onmouseup = () => {
this.mdown = false
}
document.onmousemove = (e: MouseEvent) => {
if (this.mdown) {
// 清除文本选择
window.getSelection()?.removeAllRanges()
this.relY = e.pageY - this.newPar.getBoundingClientRect().top
this.pC = ((this.relY - this.posCorrection) * 100) / this.newPar.clientHeight
if (this.pC >= 0 && this.pC + this.btmCorrection <= 100) {
this.scrollBar.style.top = `${this.pC}%`
this.inP.scrollTop =
((parseFloat(this.scrollBar.style.top) -
((this.sbHeight - parseFloat(this.sB.height)) * this.inP.scrollTop) /
(this.inP.scrollHeight - this.inP.clientHeight)) *
this.inP.scrollHeight) /
100
} else if (this.pC < 0 && parseFloat(this.scrollBar.style.top) > 0) {
this.scrollBar.style.top = "0%"
this.inP.scrollTop = 0
}
if (this.onScrollF) {
this.onScrollF()
}
}
return false
if (this.onScrollF) {
this.onScrollF()
}
}
return false
}
}
public setStyle(scrollbar?: ScrollStyle, scrollbarHolder?: ScrollStyle): Scrollbot {
if (scrollbar) {
scrollbar.width = `${this.sbw}px`
if ("height" in scrollbar) {
this.customHeight = true
scrollbar.height = `${(parseFloat(scrollbar.height) * 100) / this.newPar.clientHeight}%`
}
Object.assign(this.sB, scrollbar)
Object.assign(this.scrollBar.style, scrollbar)
}
public setStyle(scrollbar?: ScrollStyle, scrollbarHolder?: ScrollStyle): Scrollbot {
if (scrollbar) {
scrollbar.width = `${this.sbw}px`
if ("height" in scrollbar) {
this.customHeight = true
scrollbar.height = `${(parseFloat(scrollbar.height) * 100) / this.newPar.clientHeight}%`
}
Object.assign(this.sB, scrollbar)
Object.assign(this.scrollBar.style, scrollbar)
}
if (scrollbarHolder) {
scrollbarHolder.width = `${this.sbw}px`
Object.assign(this.sBH, scrollbarHolder)
Object.assign(this.scrollBarHolder.style, scrollbarHolder)
}
return this
if (scrollbarHolder) {
scrollbarHolder.width = `${this.sbw}px`
Object.assign(this.sBH, scrollbarHolder)
Object.assign(this.scrollBarHolder.style, scrollbarHolder)
}
return this
}
}
export default Scrollbot

39
src/renderer/src/assets/style/_common.scss

@ -1,44 +1,45 @@
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
html {
--text-normal: #6b6b6b;
--text-hover: #000000;
height: 100%;
--text-normal: #6b6b6b;
--text-hover: #000000;
height: 100%;
color: var(--text-normal);
}
body {
--at-apply: text-normal;
height: 100%;
--at-apply: text-normal;
height: 100%;
}
#app {
height: 100%;
height: 100%;
}
* {
user-select: none;
outline: none;
user-select: none;
outline: none;
}
.simplebar-scrollbar::before {
background-color: #bdbdbd;
border-radius: 0;
left: 0;
right: 0;
bottom: 0;
top: 0;
background-color: #bdbdbd;
border-radius: 0;
left: 0;
right: 0;
bottom: 0;
top: 0;
}
.simplebar-hover .simplebar-scrollbar::before {
background-color: #909090;
background-color: #909090;
}
.simplebar-wrapper:hover ~ .simplebar-track > .simplebar-scrollbar:before {
opacity: 0.5 !important;
opacity: 0.5 !important;
}

116
src/renderer/src/bridge/PopupMenu.ts

@ -4,7 +4,7 @@
* @homepage: https://oldj.net
*/
import { IMenuItemOption } from "#"
import { IMenuItemOption } from "#/popup-menu"
import type { PopupOptions } from "electron"
let _idx: number = 0
@ -12,58 +12,74 @@ let _idx: number = 0
type OffFunction = () => void
export class PopupMenu {
private _id: string
private _items: IMenuItemOption[]
private _offs: any[] = []
private _id: string
private _items: IMenuItemOption[]
private _offs: any[] = []
private clickEvent: Function = ()=>{}
constructor(menu_items: IMenuItemOption[]) {
this._id = `popup_menu_${Math.floor(Math.random() * 1e8)}`
this._items = menu_items
}
constructor(menu_items: IMenuItemOption[]) {
this._id = `popup_menu_${Math.floor(Math.random() * 1e8)}`
this._items = menu_items
}
show(popupOptions?: PopupOptions) {
// console.log('show')
this.onHide()
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
function readMenu(_items: IMenuItemOption[]) {
return _items.map(i => {
const d = { ...i }
if (typeof d.click === "function") {
const r = Math.floor(Math.random() * 1e8)
const evt = `popup_menu_item_${_idx++}_${r}`
const off = api.once(evt, d.click as any)
that._offs.push(off)
d._click_evt = evt
delete d.click
}
if (d.submenu && Array.isArray(d.submenu)) {
d.submenu = readMenu(d.submenu)
}
return d
})
}
const items = readMenu(this._items)
setClickEvent(fn: Function) {
this.clickEvent = fn
}
// popupOptions 中的 x,y 必须为整数
api.popupMenu({
menu_id: this._id,
items,
popupOptions,
})
;((offs: OffFunction[]) => {
api.once(`popup_menu_close:${this._id}`, () => {
// console.log(`on popup_menu_close:${this._id}`)
setTimeout(() => {
offs.map(o => o())
}, 100)
})
})(this._offs)
show(popupOptions?: PopupOptions) {
// console.log('show')
this.onHide()
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
function readMenu(_items: IMenuItemOption[]) {
return _items.map(i => {
const d = { ...i }
if(!d.click) {
const r = Math.floor(Math.random() * 1e8)
const evt = `popup_menu_item_${_idx++}_${r}`
const off = api.once(evt, (...argus)=>{
console.log(1231);
that.clickEvent(i, ...argus)
})
that._offs.push(off)
d._click_evt = evt
delete d.click
}
if (typeof d.click === "function") {
const r = Math.floor(Math.random() * 1e8)
const evt = `popup_menu_item_${_idx++}_${r}`
const off = api.once(evt, d.click as any)
that._offs.push(off)
d._click_evt = evt
delete d.click
}
if (d.submenu && Array.isArray(d.submenu)) {
d.submenu = readMenu(d.submenu)
}
return d
})
}
const items = readMenu(this._items)
private onHide() {
// console.log('hide...')
this._offs.map(o => o())
this._offs = []
}
// popupOptions 中的 x,y 必须为整数
api.popupMenu({
menu_id: this._id,
items,
popupOptions,
})
;((offs: OffFunction[]) => {
api.once(`popup_menu_close:${this._id}`, () => {
// console.log(`on popup_menu_close:${this._id}`)
setTimeout(() => {
offs.map(o => o())
}, 100)
})
})(this._offs)
}
private onHide() {
// console.log('hide...')
this._offs.map(o => o())
this._offs = []
}
}

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

Loading…
Cancel
Save