Browse Source

style: 统一代码缩进为2个空格,提升代码可读性

此次提交主要将代码中的缩进统一调整为2个空格,替换原有的4个空格或Tab缩进。这一改动不影响代码功能,但有助于提升代码的一致性和可读性。
feat/icon
npmrun 2 weeks ago
parent
commit
b6964f5fbe
  1. 60
      .prettierrc
  2. 2
      .vscode/extensions.json
  3. 74
      .vscode/launch.json
  4. 32
      .vscode/settings.json
  5. 54
      config/index.ts
  6. 62
      electron-builder.yml
  7. 180
      electron.vite.config.ts
  8. 152
      package.json
  9. 2
      pnpm-workspace.yaml
  10. 17
      resources/fuck.html
  11. 8
      src/common/_ioc.main.ts
  12. 36
      src/common/event/PlatForm/index.ts
  13. 124
      src/common/event/PlatForm/main/command.ts
  14. 100
      src/common/event/Tabs/index.ts
  15. 88
      src/common/event/Tabs/main/command.ts
  16. 6
      src/common/event/update/index.ts
  17. 8
      src/common/event/update/main/command.ts
  18. 2
      src/common/event/update/main/index.ts
  19. 16
      src/common/lib/_Base.ts
  20. 78
      src/common/lib/abstract.ts
  21. 44
      src/common/lib/browser.ts
  22. 26
      src/common/lib/electron.ts
  23. 8
      src/common/usePlatform.ts
  24. 278
      src/main/App copy.ts
  25. 120
      src/main/App.ts
  26. 6
      src/main/_ioc.ts
  27. 24
      src/main/_iocClass.ts
  28. 6
      src/main/base/base.ts
  29. 30
      src/main/controller/BasicService.ts
  30. 54
      src/main/controller/TabsService.ts
  31. 6
      src/main/controller/_ioc.ts
  32. 90
      src/main/index.ts
  33. 38
      src/main/modules/_ioc.ts
  34. 152
      src/main/modules/api/index.ts
  35. 1
      src/main/modules/api/readme.md
  36. 1
      src/main/modules/api/test.ts
  37. 166
      src/main/modules/commands/index.ts
  38. 44
      src/main/modules/db/custom.ts
  39. 116
      src/main/modules/db/index.ts
  40. 356
      src/main/modules/setting/index.ts
  41. 8
      src/main/modules/tabs/Constant.ts
  42. 484
      src/main/modules/tabs/Tab.ts
  43. 204
      src/main/modules/tabs/index.ts
  44. 126
      src/main/modules/updater/hot/index.ts
  45. 218
      src/main/modules/updater/index.ts
  46. 612
      src/main/modules/window-manager/index.ts
  47. 216
      src/main/modules/window-manager/windowsMap.ts
  48. 734
      src/main/modules/zephyr/index.ts
  49. 28
      src/main/utils/index.ts
  50. 106
      src/preload/call.ts
  51. 84
      src/preload/index.ts
  52. 48
      src/renderer/about.html
  53. 1177
      src/renderer/auto-imports.d.ts
  54. 27
      src/renderer/index.html
  55. 32
      src/renderer/src/App.vue
  56. 427
      src/renderer/src/assets/libs/scrollbot.ts
  57. 38
      src/renderer/src/assets/style/_common.scss
  58. 98
      src/renderer/src/bridge/PopupMenu.ts
  59. 758
      src/renderer/src/components/AdjustLine.vue
  60. 86
      src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts
  61. 2
      src/renderer/src/components/CodeEditor/a.d.ts
  62. 441
      src/renderer/src/components/CodeEditor/code-editor.vue
  63. 2
      src/renderer/src/components/CodeEditor/readme.md
  64. 58
      src/renderer/src/components/CodeEditor/utils.ts
  65. 138
      src/renderer/src/components/NavBar.vue
  66. 14
      src/renderer/src/components/Versions.vue
  67. 2
      src/renderer/src/composables/useTest.ts
  68. 8
      src/renderer/src/env.d.ts
  69. 14
      src/renderer/src/i18n/index.ts
  70. 12
      src/renderer/src/layouts/default.vue
  71. 8
      src/renderer/src/main.ts
  72. 2
      src/renderer/src/pages/[...all].vue
  73. 14
      src/renderer/src/pages/_ui/App.vue
  74. 404
      src/renderer/src/pages/_ui/Browser.vue
  75. 58
      src/renderer/src/pages/about/index.vue
  76. 218
      src/renderer/src/pages/browser.vue
  77. 42
      src/renderer/src/pages/index.vue
  78. 6
      src/renderer/src/router/index.ts
  79. 2
      src/renderer/src/shims.d.ts
  80. 8
      src/renderer/src/utils/index.ts
  81. 25
      src/types/global.d.ts
  82. 10
      src/types/popup-menu.ts
  83. 136
      uno.config.ts
  84. 2
      vue-macros.config.ts
  85. 4
      推荐.md

60
.prettierrc

@ -1,32 +1,32 @@
{
"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
}
}
]
"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", "lokalise.i18n-ally"]
"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,18 +1,18 @@
{
"[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"]
"[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"]
}

54
config/index.ts

@ -7,38 +7,38 @@ type LogoType = "logo" | "bg"
// 配置接口定义
interface IDefaultConfig {
language: LanguageType
"common.theme": ThemeType
"desktop:wallpaper": string
"update.repo"?: string
"update.owner"?: string
"update.allowDowngrade": boolean
"update.allowPrerelease": boolean
"editor.bg": string
"editor.logoType": LogoType
"editor.fontFamily": string
storagePath: string
language: LanguageType
"common.theme": ThemeType
"desktop:wallpaper": string
"update.repo"?: string
"update.owner"?: string
"update.allowDowngrade": boolean
"update.allowPrerelease": boolean
"editor.bg": string
"editor.logoType": LogoType
"editor.fontFamily": string
storagePath: string
}
interface IConfig {
app_title: string
default_config: IDefaultConfig
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,
},
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 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

@ -13,100 +13,100 @@ import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite"
import monacoEditorPlugin from "vite-plugin-monaco-editor"
export default defineConfig({
main: {
resolve: {
alias: {
config: resolve("config"),
main: resolve("src/main"),
common: resolve("src/common"),
"@res": resolve("resources"),
},
},
plugins: [externalizeDepsPlugin()],
main: {
resolve: {
alias: {
config: resolve("config"),
main: resolve("src/main"),
common: resolve("src/common"),
"@res": resolve("resources"),
},
},
preload: {
plugins: [externalizeDepsPlugin()],
plugins: [externalizeDepsPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()],
},
renderer: {
root: resolve(__dirname, "./src/renderer"),
resolve: {
alias: {
config: resolve("config"),
common: resolve("src/common"),
"@": resolve("src/renderer/src"),
"@res": resolve("resources"),
},
},
renderer: {
root: resolve(__dirname, "./src/renderer"),
resolve: {
alias: {
config: resolve("config"),
common: resolve("src/common"),
"@": 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`,
api: "modern-compiler",
},
},
},
},
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"],
}),
},
}),
VueI18nPlugin({
compositionOnly: false,
include: resolve(__dirname, "packages/locales/languages/**"),
}),
Layouts({
layoutsDirs: "src/layouts",
pagesDirs: "src/pages",
defaultLayout: "default",
extensions: ["vue", "setup.tsx"],
exclude: ["**/_ui"],
}),
// https://github.com/antfu/unplugin-auto-import
AutoImport({
imports: [
"vue",
"@vueuse/core",
VueRouterAutoImports,
{
// add any other imports you were relying on
"vue-router/auto": ["useLink"],
},
"vue-i18n",
],
dts: true,
dirs: ["src/composables"],
vueTemplate: true,
}),
// https://github.com/antfu/vite-plugin-components
Components({
dts: true,
dirs: ["src/components"],
}),
// https://wf0.github.io/example/plugins/Formatter.html
// @ts-ignore ...
monacoEditorPlugin.default({
publicPath: "monacoeditorwork",
customDistPath() {
return resolve(__dirname, "out/renderer/monacoeditorwork")
},
}),
}),
VueI18nPlugin({
compositionOnly: false,
include: resolve(__dirname, "packages/locales/languages/**"),
}),
Layouts({
layoutsDirs: "src/layouts",
pagesDirs: "src/pages",
defaultLayout: "default",
extensions: ["vue", "setup.tsx"],
exclude: ["**/_ui"],
}),
// https://github.com/antfu/unplugin-auto-import
AutoImport({
imports: [
"vue",
"@vueuse/core",
VueRouterAutoImports,
{
// add any other imports you were relying on
"vue-router/auto": ["useLink"],
},
"vue-i18n",
],
},
dts: true,
dirs: ["src/composables"],
vueTemplate: true,
}),
// https://github.com/antfu/vite-plugin-components
Components({
dts: true,
dirs: ["src/components"],
}),
// https://wf0.github.io/example/plugins/Formatter.html
// @ts-ignore ...
monacoEditorPlugin.default({
publicPath: "monacoeditorwork",
customDistPath() {
return resolve(__dirname, "out/renderer/monacoeditorwork")
},
}),
],
},
})

152
package.json

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

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>

8
src/common/_ioc.main.ts

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

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

@ -2,29 +2,29 @@ import { _Base } from "common/lib/_Base"
import { ApiFactory } from "common/lib/abstract"
class PlatForm extends _Base {
constructor() {
super()
}
constructor() {
super()
}
private get api() {
return ApiFactory.getApiClient()
}
private get api() {
return ApiFactory.getApiClient()
}
async showAbout() {
return this.api.call("BasicService.showAbout")
}
async showAbout() {
return this.api.call("BasicService.showAbout")
}
async isFullScreen() {
return this.api.call("PlatFormCommand.isFullscreen")
}
async isFullScreen() {
return this.api.call("PlatFormCommand.isFullscreen")
}
async toggleFullScreen() {
return this.api.call("PlatFormCommand.fullscreen")
}
async toggleFullScreen() {
return this.api.call("PlatFormCommand.fullscreen")
}
async toggleDevTools() {
return this.api.call("PlatFormCommand.toggleDevTools")
}
async toggleDevTools() {
return this.api.call("PlatFormCommand.toggleDevTools")
}
}
export { PlatForm }

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

@ -4,78 +4,78 @@ import Tabs from "main/modules/tabs"
import WindowManager from "main/modules/window-manager"
export default class PlatFormCommand {
constructor(
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Tabs) private _Tabs: Tabs,
) {}
constructor(
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Tabs) private _Tabs: Tabs,
) {}
setTheme(theme: typeof nativeTheme.themeSource) {
nativeTheme.themeSource = theme
}
setTheme(theme: typeof nativeTheme.themeSource) {
nativeTheme.themeSource = theme
}
setTitlBar(options: TitleBarOverlayOptions) {
const mainWindow = this._WindowManager.getMainWindow()
if (mainWindow) {
mainWindow.setTitleBarOverlay(options)
}
setTitlBar(options: TitleBarOverlayOptions) {
const mainWindow = this._WindowManager.getMainWindow()
if (mainWindow) {
mainWindow.setTitleBarOverlay(options)
}
}
showAbout() {
this._WindowManager.showWindow("about")
}
showAbout() {
this._WindowManager.showWindow("about")
}
toggleDevTools() {
const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) {
// @ts-ignore ...
focusedWindow.toggleDevTools()
}
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)
}
}
fullscreen() {
const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) {
const isFullScreen = focusedWindow!.isFullScreen()
focusedWindow!.setFullScreen(!isFullScreen)
}
}
isFullscreen() {
const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) {
return focusedWindow!.isFullScreen()
}
return false
isFullscreen() {
const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) {
return focusedWindow!.isFullScreen()
}
return false
}
relunch() {
app.relaunch()
app.exit()
}
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()
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()
}
}

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

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

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

@ -4,61 +4,61 @@ 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)
}
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())
}
listenerTabActive = () => {
broadcast("main:TabsCommand.update", this.getAllTabs())
}
bindElement(rect) {
this._Tabs.updateRect(rect)
}
bindElement(rect) {
this._Tabs.updateRect(rect)
}
reload() {
this._WindowManager.getMainWindow()?.reload()
}
reload() {
this._WindowManager.getMainWindow()?.reload()
}
sync() {
this.listenerTabActive()
if (!this.getAllTabs().length) {
this.add("about:blank")
}
sync() {
this.listenerTabActive()
if (!this.getAllTabs().length) {
this.add("about:blank")
}
}
add(url) {
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!)
}
add(url) {
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!)
}
nagivate(index: number, url: string) {
this._Tabs.navigate(+index, url)
}
nagivate(index: number, url: string) {
this._Tabs.navigate(+index, url)
}
closeAll() {
this._Tabs.closeAll()
}
closeAll() {
this._Tabs.closeAll()
}
setActive(index) {
this._Tabs.changeActive(index)
}
setActive(index) {
this._Tabs.changeActive(index)
}
closeTab(e) {
this._Tabs.remove(e.body.active)
}
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,
}))
}
getAllTabs() {
return this._Tabs._tabs.map(v => ({
url: v.url,
showUrl: v.showUrl,
title: v.title,
favicons: v.favicons,
isActive: v.isActive,
}))
}
}
export { TabsCommand }

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

@ -6,9 +6,9 @@ const curProgress = ref(0)
// })
function useUpdate() {
return {
curProgress,
}
return {
curProgress,
}
}
export { useUpdate }

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

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

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

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

16
src/common/lib/_Base.ts

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

78
src/common/lib/abstract.ts

@ -3,51 +3,51 @@ 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
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`)
}
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
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
}
}

44
src/common/lib/browser.ts

@ -1,29 +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())
}
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)
}
// 实现其他方法...
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)
}
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)
}
offAll<K extends string>(channel: K): void {
// 相应的全部解绑实现
console.log("不支持 on 方法", channel)
}
}

26
src/common/lib/electron.ts

@ -1,20 +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)
}
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)
}
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)
}
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)
}
offAll<K extends string>(channel: K): void {
window.api.offAll(channel)
}
}

8
src/common/usePlatform.ts

@ -2,8 +2,8 @@ import { PlatForm } from "./event/PlatForm"
import { Tabs } from "./event/Tabs"
export function usePlatForm() {
return {
Tabs: Tabs.getInstance<Tabs>(),
PlatForm: PlatForm.getInstance<PlatForm>(),
}
return {
Tabs: Tabs.getInstance<Tabs>(),
PlatForm: PlatForm.getInstance<PlatForm>(),
}
}

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

120
src/main/App.ts

@ -13,74 +13,74 @@ import Zephyr from "./modules/zephyr"
import Updater from "./modules/updater"
protocol.registerSchemesAsPrivileged([
// {
// scheme: "http",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// {
// scheme: "https",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// { scheme: "mailto", privileges: { standard: true } },
{
scheme: "api",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
// {
// scheme: "http",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// {
// scheme: "https",
// privileges: { standard: true, bypassCSP: true, allowServiceWorkers: true, supportFetchAPI: true, corsEnabled: true, stream: true },
// },
// { scheme: "mailto", privileges: { standard: true } },
{
scheme: "api",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
{
scheme: "zephyr",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
},
{
scheme: "zephyr",
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
},
},
])
@injectable()
class App extends BaseClass {
destroy() {
this._IOC.destroy()
// 这里是应用正常退出, 可以检测应用是不是非正常退出,比如应用启动时记录一个启动时间并删除上一次结束时间和开始时间,结束时记录一个结束时间,
// 如果存在结束时间或者不存在开始时间则为正常启动
}
destroy() {
this._IOC.destroy()
// 这里是应用正常退出, 可以检测应用是不是非正常退出,比如应用启动时记录一个启动时间并删除上一次结束时间和开始时间,结束时记录一个结束时间,
// 如果存在结束时间或者不存在开始时间则为正常启动
}
constructor(
@inject(IOC) private _IOC: IOC,
@inject(Api) private _Api: Api,
@inject(Command) private _Command: Command,
@inject(DB) private _DB: DB,
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Zephyr) private _Zephyr: Zephyr,
@inject(Updater) private _Updater: Updater,
) {
super()
}
constructor(
@inject(IOC) private _IOC: IOC,
@inject(Api) private _Api: Api,
@inject(Command) private _Command: Command,
@inject(DB) private _DB: DB,
@inject(WindowManager) private _WindowManager: WindowManager,
@inject(Zephyr) private _Zephyr: Zephyr,
@inject(Updater) private _Updater: Updater,
) {
super()
}
async init() {
this._Updater.init()
this._DB.init()
this._Command.init()
this._WindowManager.init()
app.whenReady().then(() => {
this._Api.init()
this._Zephyr.init()
electronApp.setAppUserModelId("top.xieyaxin")
this._WindowManager.showMainWindow()
this._Command.invoke("PlatFormCommand.setTheme", "light")
this._Command.invoke("PlatFormCommand.setTitlBar", {
height: 29,
color: "#F8F8F8",
symbolColor: "#000000",
})
})
app.on("will-quit", () => {
this.destroy()
})
}
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()
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

6
src/main/_ioc.ts

@ -6,9 +6,9 @@ import iocCommand, { destroyAllCommand } from "common/_ioc.main"
import App from "./App"
async function destroyAll() {
await destroyAllModules(_ioc)
await destroyAllController(_ioc)
await destroyAllCommand(_ioc)
await destroyAllModules(_ioc)
await destroyAllController(_ioc)
await destroyAllCommand(_ioc)
}
const _ioc = new Container()

24
src/main/_iocClass.ts

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

6
src/main/base/base.ts

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

30
src/main/controller/BasicService.ts

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

54
src/main/controller/TabsService.ts

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

6
src/main/controller/_ioc.ts

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

90
src/main/index.ts

@ -19,56 +19,56 @@ const streams = new Map()
// 转换命名空间为安全路径
function sanitizeNamespace(namespace) {
return namespace
.split(":") // 按层级分隔符拆分
.map(part => part.replace(/[\\/:*?"<>|]/g, "_")) // 替换非法字符
.join(path.sep) // 拼接为系统路径分隔符(如 / 或 \)
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
}
// 保留原始控制台输出(可选)
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`)
// 生成日志文件路径(示例: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 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 }) // 自动创建多级目录
}
// 确保目录存在
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)
}
// 获取或创建文件流
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 message = args.join(" ")
stream.write(`${message}\n`)
// const timestamp = new Date().toISOString()
// stream.write(`[${timestamp}] ${message}\n`)
// const timestamp = new Date().toISOString()
// stream.write(`[${timestamp}] ${message}\n`)
}
const curApp = _ioc.get(App)
@ -76,10 +76,10 @@ curApp.init()
const _debug = debug("app:app")
app.on("before-quit", () => {
_debug("应用关闭")
streams.forEach(stream => {
stream.end()
stream.destroy()
})
streams.clear()
_debug("应用关闭")
streams.forEach(stream => {
stream.end()
stream.destroy()
})
streams.clear()
})

38
src/main/modules/_ioc.ts

@ -9,28 +9,28 @@ 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(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()
})
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(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)
}
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);

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

@ -7,99 +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]
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]
}
public async invoke(command, ...argus) {
const result = await this.handleCommand(command, ...argus)
return result
}
public async invoke(command, ...argus) {
const result = await this.handleCommand(command, ...argus)
return result
}
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)
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
}
})
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)
}
})
} 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 = ""
}

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

@ -9,73 +9,73 @@ const debug = _debug("app:db")
@injectable()
class DB extends BaseClass {
destroy() {
debug(`DB destroy`)
}
Modules: Record<string, CustomLow<any>> = {}
destroy() {
debug(`DB destroy`)
}
Modules: Record<string, CustomLow<any>> = {}
constructor(@inject(Setting) private _setting: Setting) {
super()
}
constructor(@inject(Setting) private _setting: Setting) {
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(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]
}
}
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

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

@ -18,217 +18,217 @@ 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
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])
}
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)
// 使用正则表达式检查字符串是否以斜杠或盘符开头
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"])
// 在配置初始化后执行
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
}
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)])
}
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)
}
}
}
#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")
}
#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, {})
}
/**
*
* @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]
}
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)
}
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()
}
config() {
return this.#config
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)
}
#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)
if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) {
fs.moveSync(storagePath, p, { overwrite: true })
}
#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
}
fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" })
}
reset(key: keyof IConfig) {
this.set(key, cloneDeep(_tempConfig[key]))
return false
}
return true
}
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
}
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 {
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
}
}
}
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
}
if (isChange) {
this.#sync()
this.#runCB(this.#config, oldMainConfig, changeKeys)
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
}
}
}
}
values<T extends keyof IConfig>(key: T): IConfig[T] {
return this.#config[key]
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

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

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

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

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

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

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

@ -10,15 +10,15 @@ import _debug from "debug"
const debug = _debug("app:hot-updater")
function getUpdateScriptTemplate() {
return process.platform === "win32"
? `
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}}"
@ -28,15 +28,15 @@ function getUpdateScriptTemplate() {
}
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 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
const scriptPath = path.join(os.tmpdir(), `update.${process.platform === "win32" ? "bat" : "sh"}`)
fs.writeFileSync(scriptPath, scriptContent)
return scriptPath
}
// 标记是否需要热更新
let shouldPerformHotUpdate = false
@ -45,74 +45,74 @@ 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()
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)
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()
// 执行脚本
const child = spawn(scriptPath, [], {
detached: true,
shell: true,
env: {
APP_PATH: appPath,
UPDATE_DIR: updateTempDirPath,
EXE_PATH: appExePath,
EXE_NAME: exeName,
},
})
child.unref()
app.exit()
})
// 下载热更新包
export async function fetchHotUpdatePackage(updatePackageUrl: string = "https://example.com/updates/latest.zip") {
if (isReadyUpdate) return
if (isReadyUpdate) return
// 清除临时目录
clearUpdateTempDir()
// 创建临时目录
if (!fs.existsSync(updateTempDirPath)) {
fs.mkdirSync(updateTempDirPath, { recursive: true })
}
// 清除临时目录
clearUpdateTempDir()
// 创建临时目录
if (!fs.existsSync(updateTempDirPath)) {
fs.mkdirSync(updateTempDirPath, { recursive: true })
}
// 下载文件的本地保存路径
const downloadPath = path.join(updateTempDirPath, "update.zip")
// 下载文件的本地保存路径
const downloadPath = path.join(updateTempDirPath, "update.zip")
try {
// 使用 fetch 下载更新包
const response = await fetch(updatePackageUrl)
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
}
try {
// 使用 fetch 下载更新包
const response = await fetch(updatePackageUrl)
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
}
// 将下载内容写入文件
const arrayBuffer = await response.arrayBuffer()
fs.writeFileSync(downloadPath, Buffer.from(arrayBuffer))
// 将下载内容写入文件
const arrayBuffer = await response.arrayBuffer()
fs.writeFileSync(downloadPath, Buffer.from(arrayBuffer))
// 解压更新包
await extract(downloadPath, { dir: updateTempDirPath })
// 解压更新包
await extract(downloadPath, { dir: updateTempDirPath })
// 删除下载的zip文件
fs.unlinkSync(downloadPath)
isReadyUpdate = true
emitHotUpdateReady()
} catch (error) {
debug("热更新包下载失败:", error)
throw error
}
// 删除下载的zip文件
fs.unlinkSync(downloadPath)
isReadyUpdate = true
emitHotUpdateReady()
} catch (error) {
debug("热更新包下载失败:", error)
throw error
}
}
function clearUpdateTempDir() {
if (!fs.existsSync(updateTempDirPath)) return
fs.rmSync(updateTempDirPath, { recursive: true })
if (!fs.existsSync(updateTempDirPath)) return
fs.rmSync(updateTempDirPath, { recursive: true })
}
export function flagNeedUpdate() {
shouldPerformHotUpdate = true
shouldPerformHotUpdate = true
}

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

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

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

@ -8,353 +8,353 @@ import _debug from "debug"
const debug = _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()
this.isMainShowReady = new Promise(resolve => {
this.isMainShowResolve = resolve
})
}
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
}
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)
this.isMainShowResolve()
}
private isMainShowResolve
private isMainShowReady
async waitMainShowReady() {
await this.isMainShowReady
}
private isMainShowResolve
private isMainShowReady
async waitMainShowReady() {
await this.isMainShowReady
}
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)
}
if (curConfig.url) {
browserWin.loadURL(curConfig.url)
// logger.debug(`当前窗口网址:${curConfig.url}`)
#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.windowOpts?.show === false) {
if (curConfig.url) {
browserWin.once("ready-to-show", () => {
debug(`准备展示:`, curConfig.url)
browserWin?.show()
})
} else {
browserWin?.show()
}
}
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,
})
}
return browserWin
}
showCurrentWindow() {
if (this.#windows.length) {
debug(`current open window: ${this.#windows.map(v => v.$$opts!.name).join(",")}`)
if (choice === 1) {
justQuit()
} else {
debug(`all closed`)
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
// }
#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()
}
// // 禁用 Node.js 集成
// webPreferences.nodeIntegration = false
get(name: string) {
return this.#windows.find(v => {
return v.$$opts!.name === name
// // 验证正在加载的 URL
// // if (!params.src.startsWith('https://example.com/')) {
// // event.preventDefault()
// // }
// })
if (curConfig.type === "info") {
// 隐藏菜单
browserWin.setMenuBarVisibility(false)
}
if (curConfig.url) {
browserWin.loadURL(curConfig.url)
// logger.debug(`当前窗口网址:${curConfig.url}`)
}
if (curConfig.windowOpts?.show === false) {
if (curConfig.url) {
browserWin.once("ready-to-show", () => {
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) {
debug(`current open window: ${this.#windows.map(v => v.$$opts!.name).join(",")}`)
} else {
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("该窗口不存在")
// }
// }
}

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

@ -7,124 +7,124 @@ import { join } from "path"
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: join(__dirname, "../preload/index.mjs"),
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,
},
},
},
},
"^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,
},
}
},
},
}
}

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

@ -70,407 +70,407 @@ fetch("zephyr://w/path/to/file.txt", {
@injectable()
class Zephyr extends BaseClass {
// 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 ALLOWED_PATHS: string[] = [] // 可以在这里定义允许访问的路径白名单
private readonly ALLOWED_EXTENSIONS: string[] = [".txt", ".json", ".md"] // 允许的文件类型
private readonly MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
// private readonly SAFE_PATH_PATTERN = /^[a-zA-Z0-9\s\-_\/\\:\.]+$/
// 定义操作类型
private readonly OPERATIONS = {
READ: "r",
WRITE: "w",
READWRITE: "rw",
TEMP: "t",
} as const
private readonly pathConfig: {
read: string[]
temp: string[]
write: string[]
} = {
read: [],
temp: [],
write: [],
}
// 文件锁定相关
private readonly fileLocks = new Map<string, boolean>()
// 访问频率限制相关
private readonly rateLimiter = new Map<string, number>()
private readonly MAX_REQUESTS = 10 // 每个文件在时间窗口内的最大请求次数
private readonly WINDOW_MS = 60000 // 时间窗口:1分钟
// 审计日志相关
private readonly LOG_FILE = path.join(app.getPath("logs"), "zephyr-access.log")
constructor() {
super()
this.interceptHandlerZephyr = this.interceptHandlerZephyr.bind(this)
this.initLogFile()
debug("zephyr init")
}
private async initLogFile() {
const logDir = path.dirname(this.LOG_FILE)
await fs.promises.mkdir(logDir, { recursive: true })
}
// 文件锁定机制
private async acquireFileLock(filePath: string): Promise<boolean> {
if (this.fileLocks.get(filePath)) {
return false
}
// 文件锁定相关
private readonly fileLocks = new Map<string, boolean>()
// 访问频率限制相关
private readonly rateLimiter = new Map<string, number>()
private readonly MAX_REQUESTS = 10 // 每个文件在时间窗口内的最大请求次数
private readonly WINDOW_MS = 60000 // 时间窗口:1分钟
// 审计日志相关
private readonly LOG_FILE = path.join(app.getPath("logs"), "zephyr-access.log")
constructor() {
super()
this.interceptHandlerZephyr = this.interceptHandlerZephyr.bind(this)
this.initLogFile()
debug("zephyr init")
this.fileLocks.set(filePath, true)
return true
}
private releaseFileLock(filePath: string): void {
this.fileLocks.delete(filePath)
}
// 访问频率限制
private isRateLimited(filePath: string): boolean {
// const now = Date.now()
const count = this.rateLimiter.get(filePath) || 0
if (count >= this.MAX_REQUESTS) {
debug("访问频率超限:", filePath)
return true
}
private async initLogFile() {
const logDir = path.dirname(this.LOG_FILE)
await fs.promises.mkdir(logDir, { recursive: 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,
}
// 文件锁定机制
private async acquireFileLock(filePath: string): Promise<boolean> {
if (this.fileLocks.get(filePath)) {
return false
}
this.fileLocks.set(filePath, true)
return true
try {
await fs.promises.appendFile(this.LOG_FILE, JSON.stringify(logEntry) + "\n", "utf8")
} catch (error) {
debug("写入审计日志失败:", error)
}
private releaseFileLock(filePath: string): void {
this.fileLocks.delete(filePath)
}
// 文件内容验证
private async validateFileContent(filePath: string): Promise<boolean> {
try {
const ext = path.extname(filePath).toLowerCase()
const content = await fs.promises.readFile(filePath, "utf8")
switch (ext) {
case ".json":
JSON.parse(content)
return true
case ".md":
// 可以添加 Markdown 验证逻辑
return content.length > 0
case ".txt":
// 文本文件验证
return content.length > 0
default:
return false
}
} catch {
return false
}
}
destroy() {
const ses = session.defaultSession
ses.protocol.unhandle("zephyr")
this.fileLocks.clear()
this.rateLimiter.clear()
debug("zephyr destroyed")
}
init(partition?: string) {
const ses = partition ? session.fromPartition(partition) : session.defaultSession
ses.protocol.handle("zephyr", this.interceptHandlerZephyr)
debug("zephyr initialized with partition:", partition)
}
setAllowedPaths(config: Partial<typeof this.pathConfig>) {
Object.assign(this.pathConfig, config)
debug("Updated allowed paths:", this.pathConfig)
}
private isValidPath(filePath: string): boolean {
try {
// 规范化路径
const normalizedPath = path.normalize(filePath)
// Windows 路径特殊处理
const isWindowsPath = /^[a-z]:/i.test(normalizedPath)
// 检查基本字符(排除特殊字符)
// 允许驱动器冒号,但排除其他特殊字符
const basicCheck = isWindowsPath ? /^[a-z]:[^<>"|?*]+$/i.test(normalizedPath) : /^[^<>:"|?*]+$/i.test(normalizedPath)
return basicCheck && (isWindowsPath || normalizedPath.startsWith("/"))
} catch {
return false
}
}
// 访问频率限制
private isRateLimited(filePath: string): boolean {
// const now = Date.now()
const count = this.rateLimiter.get(filePath) || 0
private async isPathSafe(filePath: string, operation: string): Promise<boolean> {
try {
// 1. 基本路径检查
if (!this.isValidPath(filePath)) {
debug("不安全的路径字符:", filePath)
return false
}
if (count >= this.MAX_REQUESTS) {
debug("访问频率超限:", filePath)
return true
}
// 2. 检查是否包含 .. 路径
if (filePath.includes("..")) {
debug("检测到路径遍历尝试")
return false
}
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)
// 3. 检查符号链接
if (await this.isSymlink(filePath)) {
debug("不允许访问符号链接")
return false
}
// 4. 检查文件大小
if (!(await this.checkFileSize(filePath))) {
debug("文件超出大小限制")
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,
}
// 5. 文件类型检查
const ext = path.extname(filePath).toLowerCase()
if (!this.ALLOWED_EXTENSIONS.includes(ext)) {
debug("不允许的文件类型:", ext)
return false
}
try {
await fs.promises.appendFile(this.LOG_FILE, JSON.stringify(logEntry) + "\n", "utf8")
} catch (error) {
debug("写入审计日志失败:", error)
}
}
// 6. 权限检查
const allowedPaths = this.getPathsByOperation(operation)
if (!allowedPaths) return false
// 文件内容验证
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
}
}
// 7. 确保路径在允许范围内
const isInAllowedPath = allowedPaths.some(allowedPath => {
const resolvedAllowed = path.resolve(allowedPath)
const resolvedTarget = path.resolve(filePath)
return resolvedTarget.startsWith(resolvedAllowed)
})
destroy() {
const ses = session.defaultSession
ses.protocol.unhandle("zephyr")
this.fileLocks.clear()
this.rateLimiter.clear()
debug("zephyr destroyed")
}
if (!isInAllowedPath) {
debug("路径不在允许范围内")
return false
}
init(partition?: string) {
const ses = partition ? session.fromPartition(partition) : session.defaultSession
ses.protocol.handle("zephyr", this.interceptHandlerZephyr)
debug("zephyr initialized with partition:", partition)
}
// 添加频率限制检查
if (this.isRateLimited(filePath)) {
await this.logAccess(operation, filePath, false, "访问频率超限")
return false
}
setAllowedPaths(config: Partial<typeof this.pathConfig>) {
Object.assign(this.pathConfig, config)
debug("Updated allowed paths:", this.pathConfig)
// 添加文件内容验证
if (!(await this.validateFileContent(filePath))) {
await this.logAccess(operation, filePath, false, "文件内容验证失败")
return false
}
await this.logAccess(operation, filePath, true)
return true
} catch (error: any) {
await this.logAccess(operation, filePath, false, error.message)
debug("路径安全检查错误:", error)
return false
}
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
}
}
async interceptHandlerZephyr(request: Request): Promise<Response> {
try {
if (!request.url.startsWith("zephyr://")) {
return net.fetch(request.url, request)
}
const urlParts = request.url.replace(/^zephyr:\/\//, "").split("/")
const operation = urlParts[0]
const filePath = path.normalize(urlParts.slice(1).join("/"))
if (!operation || !filePath) {
return new Response("Invalid URL format", { status: 400 })
}
if (!(await this.isPathSafe(filePath, operation))) {
debug("访问被拒绝:", filePath)
return new Response("Access Denied", { status: 403 })
}
// 处理不同的操作类型
switch (operation) {
case this.OPERATIONS.READ:
return await this.handleReadOperation(filePath)
case this.OPERATIONS.WRITE:
return await this.handleWriteOperation(filePath, request)
default:
return new Response("Operation not supported", { status: 400 })
}
} catch (error) {
debug("处理请求错误:", error)
return new Response("Internal Server Error", { status: 500 })
}
private async isPathSafe(filePath: string, operation: string): Promise<boolean> {
try {
// 1. 基本路径检查
if (!this.isValidPath(filePath)) {
debug("不安全的路径字符:", filePath)
return false
}
// 2. 检查是否包含 .. 路径
if (filePath.includes("..")) {
debug("检测到路径遍历尝试")
return false
}
// 3. 检查符号链接
if (await this.isSymlink(filePath)) {
debug("不允许访问符号链接")
return false
}
// 4. 检查文件大小
if (!(await this.checkFileSize(filePath))) {
debug("文件超出大小限制")
return false
}
// 5. 文件类型检查
const ext = path.extname(filePath).toLowerCase()
if (!this.ALLOWED_EXTENSIONS.includes(ext)) {
debug("不允许的文件类型:", ext)
return false
}
// 6. 权限检查
const allowedPaths = this.getPathsByOperation(operation)
if (!allowedPaths) return false
// 7. 确保路径在允许范围内
const isInAllowedPath = allowedPaths.some(allowedPath => {
const resolvedAllowed = path.resolve(allowedPath)
const resolvedTarget = path.resolve(filePath)
return resolvedTarget.startsWith(resolvedAllowed)
})
if (!isInAllowedPath) {
debug("路径不在允许范围内")
return false
}
// 添加频率限制检查
if (this.isRateLimited(filePath)) {
await this.logAccess(operation, filePath, false, "访问频率超限")
return false
}
// 添加文件内容验证
if (!(await this.validateFileContent(filePath))) {
await this.logAccess(operation, filePath, false, "文件内容验证失败")
return false
}
await this.logAccess(operation, filePath, true)
return true
} catch (error: any) {
await this.logAccess(operation, filePath, false, error.message)
debug("路径安全检查错误:", error)
return false
}
}
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 })
}
async interceptHandlerZephyr(request: Request): Promise<Response> {
try {
if (!request.url.startsWith("zephyr://")) {
return net.fetch(request.url, request)
}
const urlParts = request.url.replace(/^zephyr:\/\//, "").split("/")
const operation = urlParts[0]
const filePath = path.normalize(urlParts.slice(1).join("/"))
if (!operation || !filePath) {
return new Response("Invalid URL format", { status: 400 })
}
if (!(await this.isPathSafe(filePath, operation))) {
debug("访问被拒绝:", filePath)
return new Response("Access Denied", { status: 403 })
}
// 处理不同的操作类型
switch (operation) {
case this.OPERATIONS.READ:
return await this.handleReadOperation(filePath)
case this.OPERATIONS.WRITE:
return await this.handleWriteOperation(filePath, request)
default:
return new Response("Operation not supported", { status: 400 })
}
} catch (error) {
debug("处理请求错误:", error)
return new Response("Internal Server Error", { status: 500 })
}
try {
if (!(await this.acquireFileLock(filePath))) {
await this.logAccess("READ", filePath, false, "文件已锁定")
return new Response("File is locked", { status: 423 })
}
const stream = fs.createReadStream(filePath, {
flags: "r",
encoding: "utf8",
})
const timeout = setTimeout(() => {
stream.destroy()
cleanup(new Error("读取超时"))
}, 5000)
const response = new Response(stream as any, {
status: 200,
headers: {
"content-type": this.getContentType(filePath),
"cache-control": "no-cache",
},
})
stream.on("error", error => {
clearTimeout(timeout)
cleanup(error)
})
stream.on("end", () => {
clearTimeout(timeout)
cleanup()
})
return response
} catch (error) {
return await cleanup(error as Error)
}
}
private getContentType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase()
const contentTypes: Record<string, string> = {
".txt": "text/plain",
".json": "application/json",
".md": "text/markdown",
}
return contentTypes[ext] || "application/octet-stream"
}
private async isSymlink(filePath: string): Promise<boolean> {
try {
const stats = await fs.promises.lstat(filePath)
return stats.isSymbolicLink()
} catch {
return false
}
}
private async checkFileSize(filePath: string): Promise<boolean> {
try {
const stats = await fs.promises.stat(filePath)
return stats.size <= this.MAX_FILE_SIZE
} catch {
return false
}
}
private getPathsByOperation(operation: string): string[] | null {
switch (operation) {
case this.OPERATIONS.READ:
return this.pathConfig.read
case this.OPERATIONS.TEMP:
return this.pathConfig.temp
case this.OPERATIONS.WRITE:
return this.pathConfig.write
default:
debug("未知的操作类型:", operation)
return null
}
}
private async handleWriteOperation(filePath: string, request: Request): Promise<Response> {
const cleanup = async (error?: Error): Promise<Response> => {
this.releaseFileLock(filePath)
if (error) {
await this.logAccess("WRITE", filePath, false, error.message)
return new Response("Write failed: " + error.message, { status: 500 })
}
return new Response("Write successful", { status: 200 })
}
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 {
// 1. 获取文件锁
if (!(await this.acquireFileLock(filePath))) {
return new Response("File is locked", { status: 423 })
}
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)
}
}
// 2. 确保目标目录存在
await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
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"
}
// 3. 获取请求内容
const content = await request.text()
private async isSymlink(filePath: string): Promise<boolean> {
try {
const stats = await fs.promises.lstat(filePath)
return stats.isSymbolicLink()
} catch {
return false
}
}
// 4. 验证内容大小
if (content.length > this.MAX_FILE_SIZE) {
return cleanup(new Error("Content too large"))
}
private async checkFileSize(filePath: string): Promise<boolean> {
// 5. 验证文件类型和内容
const ext = path.extname(filePath).toLowerCase()
if (ext === ".json") {
try {
const stats = await fs.promises.stat(filePath)
return stats.size <= this.MAX_FILE_SIZE
JSON.parse(content)
} catch {
return false
return cleanup(new Error("Invalid JSON content"))
}
}
}
private getPathsByOperation(operation: string): string[] | null {
switch (operation) {
case this.OPERATIONS.READ:
return this.pathConfig.read
case this.OPERATIONS.TEMP:
return this.pathConfig.temp
case this.OPERATIONS.WRITE:
return this.pathConfig.write
default:
debug("未知的操作类型:", operation)
return null
}
}
// 6. 写入文件
await fs.promises.writeFile(filePath, content, "utf8")
await this.logAccess("WRITE", filePath, true)
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)
}
return cleanup()
} catch (error) {
return cleanup(error as Error)
}
}
}
export default Zephyr

28
src/main/utils/index.ts

@ -3,27 +3,27 @@ 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 isPromise(value: () => any) {
return value && Object.prototype.toString.call(value) === "[object Promise]"
return value && Object.prototype.toString.call(value) === "[object Promise]"
}
export const broadcast = <T extends string>(event: T, ...args: any[]) => {
webContents.getAllWebContents().forEach(browser => browser.send(event, ...args))
webContents.getAllWebContents().forEach(browser => browser.send(event, ...args))
}
export function slash(path: string) {
const isExtendedLengthPath = path.startsWith("\\\\?\\")
if (isExtendedLengthPath) {
return path
}
return path.replace(/\\/g, "/")
const isExtendedLengthPath = path.startsWith("\\\\?\\")
if (isExtendedLengthPath) {
return path
}
return path.replace(/\\/g, "/")
}

106
src/preload/call.ts

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

84
src/preload/index.ts

@ -3,8 +3,8 @@ import { electronAPI } from "@electron-toolkit/preload"
import { call, callLong, callSync } from "./call"
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 +19,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
}

48
src/renderer/about.html

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

1177
src/renderer/auto-imports.d.ts

File diff suppressed because it is too large

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>

32
src/renderer/src/App.vue

@ -1,30 +1,30 @@
<script setup lang="ts"></script>
<template>
<div h-full flex flex-col overflow-hidden>
<NavBar></NavBar>
<div flex-1 h-0 overflow-hidden flex flex-col>
<router-view v-slot="{ Component, route }">
<Transition name="slide-fade" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</Transition>
</router-view>
</div>
<div h-full flex flex-col overflow-hidden>
<NavBar></NavBar>
<div flex-1 h-0 overflow-hidden flex flex-col>
<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 {
.slide-fade-enter-active {
transition: all 0.2s ease-out;
}
}
.slide-fade-leave-active {
.slide-fade-leave-active {
transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1);
}
}
.slide-fade-enter-from,
.slide-fade-leave-to {
.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

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

@ -1,44 +1,44 @@
*,
*::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%;
}
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;
}

98
src/renderer/src/bridge/PopupMenu.ts

@ -12,58 +12,58 @@ 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[] = []
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
})
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
}
const items = readMenu(this._items)
// 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)
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 = []
}
}

758
src/renderer/src/components/AdjustLine.vue

@ -1,25 +1,25 @@
<template>
<div ref="adjustLineEL" :class="['adjust-line', `adjust-line--${direction}`, { 'adjust-line--dragging': isDragging }]">
<div class="adjust-line__handle">
<div class="adjust-line__grip">
<span class="grip-line"></span>
<span class="grip-line"></span>
</div>
</div>
<div ref="adjustLineEL" :class="['adjust-line', `adjust-line--${direction}`, { 'adjust-line--dragging': isDragging }]">
<div class="adjust-line__handle">
<div class="adjust-line__grip">
<span class="grip-line"></span>
<span class="grip-line"></span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, watch, computed, onBeforeUnmount, onErrorCaptured } from "vue"
import { useDebounceFn } from "@vueuse/core"
import { nextTick, onMounted, ref, watch, computed, onBeforeUnmount, onErrorCaptured } from "vue"
import { useDebounceFn } from "@vueuse/core"
const adjustLineEL = ref<HTMLElement>()
const adjustLineEL = ref<HTMLElement>()
//
type Direction = "left" | "right" | "top" | "bottom"
//
type Direction = "left" | "right" | "top" | "bottom"
// Props
interface AdjustLineProps {
// Props
interface AdjustLineProps {
/**
* 所在方向 'left' | 'right' | 'top' | 'bottom'
*/
@ -40,106 +40,106 @@ interface AdjustLineProps {
maxSize?: number
defaultSize?: number
onChange?: (size: number) => void
}
}
const props = withDefaults(defineProps<AdjustLineProps>(), {
const props = withDefaults(defineProps<AdjustLineProps>(), {
direction: "right",
minSize: 100,
maxSize: 800,
})
})
//
const emit = defineEmits<{
//
const emit = defineEmits<{
(e: "resize", size: number): void
(e: "resizeStart"): void
(e: "resizeEnd", size: number): void
}>()
let curTarget: HTMLElement | undefined | null
const isDragging = ref(false)
const currentSize = ref(props.defaultSize || 0)
// 使computed
const isHorizontal = computed(() => props.direction === "left" || props.direction === "right")
// // 使computed
// const cursorStyle = computed(() => (isHorizontal.value ? "ew-resize" : "ns-resize"))
// // localStorage
// const storageKey = computed(() => `adjust-line-${props.mid}`)
// function saveSize(size: number) {
// if (props.mid) {
// try {
// localStorage.setItem(storageKey.value, String(size))
// } catch (error) {
// console.warn("Failed to save size to localStorage:", error)
// }
// }
// }
// function loadSavedSize(): number | null {
// if (props.mid) {
// try {
// const saved = localStorage.getItem(storageKey.value)
// return saved ? Number(saved) : null
// } catch (error) {
// console.warn("Failed to load size from localStorage:", error)
// return null
// }
// }
// return null
// }
// 使resize
const emitResize = useDebounceFn((size: number) => {
}>()
let curTarget: HTMLElement | undefined | null
const isDragging = ref(false)
const currentSize = ref(props.defaultSize || 0)
// 使computed
const isHorizontal = computed(() => props.direction === "left" || props.direction === "right")
// // 使computed
// const cursorStyle = computed(() => (isHorizontal.value ? "ew-resize" : "ns-resize"))
// // localStorage
// const storageKey = computed(() => `adjust-line-${props.mid}`)
// function saveSize(size: number) {
// if (props.mid) {
// try {
// localStorage.setItem(storageKey.value, String(size))
// } catch (error) {
// console.warn("Failed to save size to localStorage:", error)
// }
// }
// }
// function loadSavedSize(): number | null {
// if (props.mid) {
// try {
// const saved = localStorage.getItem(storageKey.value)
// return saved ? Number(saved) : null
// } catch (error) {
// console.warn("Failed to load size from localStorage:", error)
// return null
// }
// }
// return null
// }
// 使resize
const emitResize = useDebounceFn((size: number) => {
emit("resize", size)
}, 16)
}, 16)
// 使ResizeObserver
let observer: ResizeObserver | null = null
const observeResize = () => {
// 使ResizeObserver
let observer: ResizeObserver | null = null
const observeResize = () => {
if (!adjustLineEL.value) return
observer = new ResizeObserver(() => {
if (curTarget) {
const size = isHorizontal.value ? curTarget.clientWidth : curTarget.clientHeight
currentSize.value = size
emitResize(size)
}
if (curTarget) {
const size = isHorizontal.value ? curTarget.clientWidth : curTarget.clientHeight
currentSize.value = size
emitResize(size)
}
})
observer.observe(adjustLineEL.value)
}
}
onBeforeUnmount(() => {
onBeforeUnmount(() => {
observer && observer.disconnect()
})
})
onMounted(async () => {
onMounted(async () => {
await nextTick()
if (!props.target) {
curTarget = adjustLineEL.value?.parentElement
curTarget = adjustLineEL.value?.parentElement
} else {
curTarget = props.target
curTarget = props.target
}
if (curTarget) {
handle(curTarget)
handle(curTarget)
}
watch(
() => props.target,
target => {
curTarget = target
if (curTarget) {
handle(curTarget)
}
},
() => props.target,
target => {
curTarget = target
if (curTarget) {
handle(curTarget)
}
},
)
observeResize()
})
})
function handle(target: HTMLElement) {
function handle(target: HTMLElement) {
if (!adjustLineEL.value) return
const nextContainer = target
const el = adjustLineEL.value
@ -148,368 +148,368 @@ function handle(target: HTMLElement) {
const watchContainer = props.watch
let isThree = false
if (container !== nextContainer) {
isThree = true
isThree = true
}
if (nextContainer && el && container && parentContainer) {
if (props.direction === "left" || props.direction === "right") {
if (props.mid) {
let w = localStorage.getItem(props.mid)
if (w != undefined) {
container.style.width = w + "px"
}
if (props.direction === "left" || props.direction === "right") {
if (props.mid) {
let w = localStorage.getItem(props.mid)
if (w != undefined) {
container.style.width = w + "px"
}
}
el.onmousedown = function (e) {
let width = container.clientWidth
let nwidth = nextContainer.clientWidth
// let owidth = nwidth + width
let owidth = parentContainer.clientWidth
let wwidth = watchContainer?.clientWidth ?? 0
if (isThree) {
owidth = nwidth + width
}
let startX = e.clientX
let lastPointerEvents = document.body.style.pointerEvents
let lastUserSelect = document.body.style.userSelect
let lastOnmousemove = document.onmousemove
let lastOnmouseup = document.onmouseup
document.onmousemove = function (e) {
let nowX = e.clientX
let w = 0
let offset = startX - nowX
if (props.direction == "left") {
w = width + offset
}
el.onmousedown = function (e) {
let width = container.clientWidth
let nwidth = nextContainer.clientWidth
// let owidth = nwidth + width
let owidth = parentContainer.clientWidth
let wwidth = watchContainer?.clientWidth ?? 0
if (isThree) {
owidth = nwidth + width
}
let startX = e.clientX
let lastPointerEvents = document.body.style.pointerEvents
let lastUserSelect = document.body.style.userSelect
let lastOnmousemove = document.onmousemove
let lastOnmouseup = document.onmouseup
document.onmousemove = function (e) {
let nowX = e.clientX
let w = 0
let offset = startX - nowX
if (props.direction == "left") {
w = width + offset
}
if (props.direction == "right") {
w = width - offset
}
if (w >= owidth) {
w = owidth
}
if (w <= 0) {
w = 0
}
// if (Math.abs(w - owidth / 2) <= 10) {
// w = owidth / 2
// }
// if (Math.abs(w - owidth) < 10) {
// w = owidth
// }
// if (Math.abs(w) < 10) {
// w = 0
// }
document.body.style.pointerEvents = "none"
document.body.style.userSelect = "none"
if (!isThree && watchContainer) {
let ww = wwidth - offset
if (width >= -offset) {
watchContainer.style.width = ww + "px"
}
nextContainer.style.width = w + "px"
} else {
if (!isThree) {
nextContainer.style.width = w + "px"
// nextContainer.style.minWidth = w + 'px'
// nextContainer.style.flexBasis = w + 'px'
} else {
nextContainer.style.width = owidth - w + "px"
// nextContainer.style.minWidth = (owidth-w) + 'px'
// nextContainer.style.flexBasis = (owidth - w) + 'px'
}
}
}
document.onmouseup = function () {
document.onmousemove = lastOnmousemove
document.onmouseup = lastOnmouseup
document.body.style.pointerEvents = lastPointerEvents
document.body.style.userSelect = lastUserSelect
if (props.mid) {
let width = container.clientWidth
localStorage.setItem(props.mid, String(width))
}
}
if (props.direction == "right") {
w = width - offset
}
}
if (props.direction === "top" || props.direction === "bottom") {
if (w >= owidth) {
w = owidth
}
if (w <= 0) {
w = 0
}
// if (Math.abs(w - owidth / 2) <= 10) {
// w = owidth / 2
// }
// if (Math.abs(w - owidth) < 10) {
// w = owidth
// }
// if (Math.abs(w) < 10) {
// w = 0
// }
document.body.style.pointerEvents = "none"
document.body.style.userSelect = "none"
if (!isThree && watchContainer) {
let ww = wwidth - offset
if (width >= -offset) {
watchContainer.style.width = ww + "px"
}
nextContainer.style.width = w + "px"
} else {
if (!isThree) {
nextContainer.style.width = w + "px"
// nextContainer.style.minWidth = w + 'px'
// nextContainer.style.flexBasis = w + 'px'
} else {
nextContainer.style.width = owidth - w + "px"
// nextContainer.style.minWidth = (owidth-w) + 'px'
// nextContainer.style.flexBasis = (owidth - w) + 'px'
}
}
}
document.onmouseup = function () {
document.onmousemove = lastOnmousemove
document.onmouseup = lastOnmouseup
document.body.style.pointerEvents = lastPointerEvents
document.body.style.userSelect = lastUserSelect
if (props.mid) {
let w = localStorage.getItem(props.mid)
if (w != undefined) {
container.style.height = w + "px"
}
let width = container.clientWidth
localStorage.setItem(props.mid, String(width))
}
el.onmousedown = function (e) {
let height = container.clientHeight
let nheight = nextContainer.clientHeight
// let oheight = nheight + height
let oheight = parentContainer.clientHeight
let hheight = watchContainer?.clientHeight ?? 0
if (isThree) {
oheight = nheight + height
}
let startY = e.clientY
let lastPointerEvents = document.body.style.pointerEvents
let lastUserSelect = document.body.style.userSelect
let lastOnmousemove = document.onmousemove
let lastOnmouseup = document.onmouseup
document.onmousemove = function (e) {
let nowY = e.clientY
let h = 0
let offset = startY - nowY
if (props.direction == "top") {
h = height + startY - nowY
}
if (props.direction == "bottom") {
h = height - offset
}
console.log(oheight)
if (h >= oheight) {
h = oheight
}
if (h <= 0) {
h = 0
}
// if (Math.abs(h - oheight / 2) <= 15) {
// h = oheight / 2
// }
// if (Math.abs(h - oheight) < 50) {
// h = oheight
// }
// if (Math.abs(h) < 50) {
// h = 0
// }
document.body.style.pointerEvents = "none"
document.body.style.userSelect = "none"
if (!isThree && watchContainer) {
let hh = hheight - offset
if (height >= -offset) {
watchContainer.style.height = hh + "px"
}
nextContainer.style.height = h + "px"
} else {
if (!isThree) {
nextContainer.style.height = h + "px"
// nextContainer.style.minHeight = h + 'px'
// nextContainer.style.flexBasis = h + 'px'
} else {
nextContainer.style.height = oheight - h + "px"
// nextContainer.style.minHeight = (oheight - h) + 'px'
// nextContainer.style.flexBasis = (oheight - h) + 'px'
}
}
}
document.onmouseup = function () {
document.onmousemove = lastOnmousemove
document.onmouseup = lastOnmouseup
document.body.style.pointerEvents = lastPointerEvents
document.body.style.userSelect = lastUserSelect
if (props.mid) {
let height = container.clientHeight
localStorage.setItem(props.mid, String(height))
}
}
}
}
}
if (props.direction === "top" || props.direction === "bottom") {
if (props.mid) {
let w = localStorage.getItem(props.mid)
if (w != undefined) {
container.style.height = w + "px"
}
}
el.onmousedown = function (e) {
let height = container.clientHeight
let nheight = nextContainer.clientHeight
// let oheight = nheight + height
let oheight = parentContainer.clientHeight
let hheight = watchContainer?.clientHeight ?? 0
if (isThree) {
oheight = nheight + height
}
let startY = e.clientY
let lastPointerEvents = document.body.style.pointerEvents
let lastUserSelect = document.body.style.userSelect
let lastOnmousemove = document.onmousemove
let lastOnmouseup = document.onmouseup
document.onmousemove = function (e) {
let nowY = e.clientY
let h = 0
let offset = startY - nowY
if (props.direction == "top") {
h = height + startY - nowY
}
if (props.direction == "bottom") {
h = height - offset
}
console.log(oheight)
if (h >= oheight) {
h = oheight
}
if (h <= 0) {
h = 0
}
// if (Math.abs(h - oheight / 2) <= 15) {
// h = oheight / 2
// }
// if (Math.abs(h - oheight) < 50) {
// h = oheight
// }
// if (Math.abs(h) < 50) {
// h = 0
// }
document.body.style.pointerEvents = "none"
document.body.style.userSelect = "none"
if (!isThree && watchContainer) {
let hh = hheight - offset
if (height >= -offset) {
watchContainer.style.height = hh + "px"
}
nextContainer.style.height = h + "px"
} else {
if (!isThree) {
nextContainer.style.height = h + "px"
// nextContainer.style.minHeight = h + 'px'
// nextContainer.style.flexBasis = h + 'px'
} else {
nextContainer.style.height = oheight - h + "px"
// nextContainer.style.minHeight = (oheight - h) + 'px'
// nextContainer.style.flexBasis = (oheight - h) + 'px'
}
}
}
document.onmouseup = function () {
document.onmousemove = lastOnmousemove
document.onmouseup = lastOnmouseup
document.body.style.pointerEvents = lastPointerEvents
document.body.style.userSelect = lastUserSelect
if (props.mid) {
let height = container.clientHeight
localStorage.setItem(props.mid, String(height))
}
}
}
}
}
}
// function handleDrag(e: MouseEvent, target: HTMLElement) {
// const startPos = isHorizontal.value ? e.clientX : e.clientY
// const startSize = isHorizontal.value ? target.clientWidth : target.clientHeight
// const handleMouseMove = (e: MouseEvent) => {
// const currentPos = isHorizontal.value ? e.clientX : e.clientY
// const diff = props.direction === "right" || props.direction === "bottom" ? startPos - currentPos : currentPos - startPos
// let newSize = startSize - diff
// //
// newSize = Math.max(props.minSize, Math.min(props.maxSize, newSize))
// //
// if (isHorizontal.value) {
// target.style.width = `${newSize}px`
// } else {
// target.style.height = `${newSize}px`
// }
// currentSize.value = newSize
// emit("resize", newSize)
// }
// const handleMouseUp = () => {
// document.removeEventListener("mousemove", handleMouseMove)
// document.removeEventListener("mouseup", handleMouseUp)
// document.body.style.userSelect = ""
// isDragging.value = false
// saveSize(currentSize.value)
// emit("resizeEnd", currentSize.value)
// }
// document.addEventListener("mousemove", handleMouseMove)
// document.addEventListener("mouseup", handleMouseUp)
// document.body.style.userSelect = "none"
// isDragging.value = true
// emit("resizeStart")
// }
const debug = {
}
// function handleDrag(e: MouseEvent, target: HTMLElement) {
// const startPos = isHorizontal.value ? e.clientX : e.clientY
// const startSize = isHorizontal.value ? target.clientWidth : target.clientHeight
// const handleMouseMove = (e: MouseEvent) => {
// const currentPos = isHorizontal.value ? e.clientX : e.clientY
// const diff = props.direction === "right" || props.direction === "bottom" ? startPos - currentPos : currentPos - startPos
// let newSize = startSize - diff
// //
// newSize = Math.max(props.minSize, Math.min(props.maxSize, newSize))
// //
// if (isHorizontal.value) {
// target.style.width = `${newSize}px`
// } else {
// target.style.height = `${newSize}px`
// }
// currentSize.value = newSize
// emit("resize", newSize)
// }
// const handleMouseUp = () => {
// document.removeEventListener("mousemove", handleMouseMove)
// document.removeEventListener("mouseup", handleMouseUp)
// document.body.style.userSelect = ""
// isDragging.value = false
// saveSize(currentSize.value)
// emit("resizeEnd", currentSize.value)
// }
// document.addEventListener("mousemove", handleMouseMove)
// document.addEventListener("mouseup", handleMouseUp)
// document.body.style.userSelect = "none"
// isDragging.value = true
// emit("resizeStart")
// }
const debug = {
log: (...args: any[]) => {
if (process.env.NODE_ENV === "development") {
console.log("[AdjustLine]", ...args)
}
if (process.env.NODE_ENV === "development") {
console.log("[AdjustLine]", ...args)
}
},
error: (...args: any[]) => {
console.error("[AdjustLine]", ...args)
console.error("[AdjustLine]", ...args)
},
}
}
function handleError(error: Error, context: string) {
function handleError(error: Error, context: string) {
debug.error(`Error in ${context}:`, error)
//
}
}
//
onErrorCaptured((err, instance, info) => {
//
onErrorCaptured((err, instance, info) => {
handleError(err as Error, info)
console.log(instance);
console.log(instance)
return false
})
})
</script>
<style lang="scss" scoped>
.adjust-line {
.adjust-line {
position: absolute;
z-index: 999;
&__handle {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
transition: all 0.2s ease;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
transition: all 0.2s ease;
}
&__grip {
display: flex;
gap: 3px;
opacity: 0;
transition: opacity 0.2s;
.adjust-line:hover &,
.adjust-line--dragging & {
opacity: 1;
}
display: flex;
gap: 3px;
opacity: 0;
transition: opacity 0.2s;
.adjust-line:hover &,
.adjust-line--dragging & {
opacity: 1;
}
}
.grip-line {
background-color: #999;
border-radius: 1px;
background-color: #999;
border-radius: 1px;
.adjust-line:hover &,
.adjust-line--dragging & {
background-color: #666;
}
.adjust-line:hover &,
.adjust-line--dragging & {
background-color: #666;
}
}
// 线
&--left,
&--right {
top: 0;
bottom: 0;
width: 10px; //
cursor: col-resize;
.adjust-line__handle {
left: 0;
top: 0;
bottom: 0;
width: 10px; //
cursor: col-resize;
.adjust-line__handle {
left: 0;
top: 0;
bottom: 0;
width: 100%;
}
width: 100%;
}
.adjust-line__grip {
flex-direction: column;
}
.adjust-line__grip {
flex-direction: column;
}
.grip-line {
width: 2px;
height: 16px;
}
.grip-line {
width: 2px;
height: 16px;
}
&:hover .adjust-line__handle {
background-color: rgba(0, 0, 0, 0.05);
}
&:hover .adjust-line__handle {
background-color: rgba(0, 0, 0, 0.05);
}
}
// 线
&--top,
&--bottom {
left: 0;
right: 0;
height: 10px; //
cursor: row-resize;
.adjust-line__handle {
top: 0;
left: 0;
right: 0;
height: 10px; //
cursor: row-resize;
.adjust-line__handle {
top: 0;
left: 0;
right: 0;
height: 100%;
}
height: 100%;
}
.adjust-line__grip {
flex-direction: row;
}
.adjust-line__grip {
flex-direction: row;
}
.grip-line {
width: 16px;
height: 2px;
}
.grip-line {
width: 16px;
height: 2px;
}
&:hover .adjust-line__handle {
background-color: rgba(0, 0, 0, 0.05);
}
&:hover .adjust-line__handle {
background-color: rgba(0, 0, 0, 0.05);
}
}
//
&--left {
left: -5px;
left: -5px;
}
&--right {
right: -5px;
right: -5px;
}
&--top {
top: -5px;
top: -5px;
}
&--bottom {
bottom: -5px;
bottom: -5px;
}
//
&--dragging {
&::after {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
cursor: inherit;
}
&::after {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
cursor: inherit;
}
.adjust-line__handle {
background-color: rgba(0, 0, 0, 0.08);
}
.adjust-line__handle {
background-color: rgba(0, 0, 0, 0.08);
}
.grip-line {
background-color: #666;
}
.grip-line {
background-color: #666;
}
}
}
}
</style>

86
src/renderer/src/components/CodeEditor/PlaceholderContentWidget.ts

@ -5,53 +5,53 @@ import { monaco } from "./monaco"
* Roughly based on https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint/untitledTextEditorHint.ts
*/
export class PlaceholderContentWidget implements monaco.editor.IContentWidget {
private static readonly ID = "editor.widget.placeholderHint"
private domNode: HTMLElement | undefined
constructor(
private readonly placeholder: string,
private readonly editor: monaco.editor.ICodeEditor,
) {
// register a listener for editor code changes
editor.onDidChangeModelContent(() => this.onDidChangeModelContent())
// ensure that on initial load the placeholder is shown
this.onDidChangeModelContent()
private static readonly ID = "editor.widget.placeholderHint"
private domNode: HTMLElement | undefined
constructor(
private readonly placeholder: string,
private readonly editor: monaco.editor.ICodeEditor,
) {
// register a listener for editor code changes
editor.onDidChangeModelContent(() => this.onDidChangeModelContent())
// ensure that on initial load the placeholder is shown
this.onDidChangeModelContent()
}
private onDidChangeModelContent(): void {
if (this.editor.getValue() === "") {
this.editor.addContentWidget(this)
} else {
this.editor.removeContentWidget(this)
}
private onDidChangeModelContent(): void {
if (this.editor.getValue() === "") {
this.editor.addContentWidget(this)
} else {
this.editor.removeContentWidget(this)
}
}
getId(): string {
return PlaceholderContentWidget.ID
}
getDomNode(): HTMLElement {
if (!this.domNode) {
this.domNode = document.createElement("div")
this.domNode.style.width = "max-content"
this.domNode.style.pointerEvents = "none"
this.domNode.textContent = this.placeholder
this.domNode.style.fontStyle = "italic"
this.editor.applyFontInfo(this.domNode)
}
getId(): string {
return PlaceholderContentWidget.ID
}
return this.domNode
}
getDomNode(): HTMLElement {
if (!this.domNode) {
this.domNode = document.createElement("div")
this.domNode.style.width = "max-content"
this.domNode.style.pointerEvents = "none"
this.domNode.textContent = this.placeholder
this.domNode.style.fontStyle = "italic"
this.editor.applyFontInfo(this.domNode)
}
return this.domNode
getPosition(): monaco.editor.IContentWidgetPosition | null {
return {
position: { lineNumber: 1, column: 1 },
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
}
}
getPosition(): monaco.editor.IContentWidgetPosition | null {
return {
position: { lineNumber: 1, column: 1 },
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
}
}
dispose(): void {
this.editor.removeContentWidget(this)
}
dispose(): void {
this.editor.removeContentWidget(this)
}
}

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

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

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

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

2
src/renderer/src/components/CodeEditor/readme.md

@ -1,3 +1,3 @@
占位符
https://github.com/Microsoft/monaco-editor/issues/1228
https://github.com/microsoft/monaco-editor/issues/568#issuecomment-1499966160
https://github.com/microsoft/monaco-editor/issues/568#issuecomment-1499966160

58
src/renderer/src/components/CodeEditor/utils.ts

@ -1,32 +1,32 @@
export function judgeFile(filename: string) {
if (!filename) return
let ext = [
{ language: "vue", ext: ".vue", index: -1 },
{ language: "javascript", ext: ".js", index: -1 },
{ language: "css", ext: ".css", index: -1 },
{ language: "html", ext: ".html", index: -1 },
{ language: "tsx", ext: ".tsx", index: -1 },
{ language: "typescript", ext: ".ts", index: -1 },
{ language: "markdown", ext: ".md", index: -1 },
{ language: "json", ext: ".json", index: -1 },
{ language: "web", ext: ".web", index: -1 },
{ language: "dot", pre: ".", index: -1 },
]
let cur
for (let i = 0; i < ext.length; i++) {
const e = ext[i]
if (e.ext && filename.endsWith(e.ext)) {
let index = filename.lastIndexOf(e.ext)
e.index = index
cur = e
break
}
if (e.pre && filename.startsWith(e.pre)) {
let index = filename.indexOf(e.pre)
e.index = index
cur = e
break
}
if (!filename) return
let ext = [
{ language: "vue", ext: ".vue", index: -1 },
{ language: "javascript", ext: ".js", index: -1 },
{ language: "css", ext: ".css", index: -1 },
{ language: "html", ext: ".html", index: -1 },
{ language: "tsx", ext: ".tsx", index: -1 },
{ language: "typescript", ext: ".ts", index: -1 },
{ language: "markdown", ext: ".md", index: -1 },
{ language: "json", ext: ".json", index: -1 },
{ language: "web", ext: ".web", index: -1 },
{ language: "dot", pre: ".", index: -1 },
]
let cur
for (let i = 0; i < ext.length; i++) {
const e = ext[i]
if (e.ext && filename.endsWith(e.ext)) {
let index = filename.lastIndexOf(e.ext)
e.index = index
cur = e
break
}
return cur
if (e.pre && filename.startsWith(e.pre)) {
let index = filename.indexOf(e.pre)
e.index = index
cur = e
break
}
}
return cur
}

138
src/renderer/src/components/NavBar.vue

@ -1,102 +1,102 @@
<template>
<div
relative
h="30px"
leading="29px"
pr="137px"
:style="{ paddingRight: isFullScreen ? '0' : '' }"
select-none
border-b="1px solid #E5E5E5"
bg="#F8F8F8"
>
<div absolute top-0 right-0 bottom-0 left-0 style="-webkit-app-region: drag"></div>
<div h-full px-2 flex items-center gap-1 justify-between>
<div flex items-center gap-1>
<img w="16px" h="16px" :src="icon" />
<div relative h-full inline-flex items-center text-sm>{{ config.app_title }}</div>
<div relative class="list">
<div class="item" @click="onClickMenu">{{ t("caidan") }}</div>
</div>
</div>
<div float-right h-full flex items-center relative style="-webkit-app-region: no-drag">
<div
v-if="!isHome"
text-sm
px-2
hover:rounded-md
hover:bg-gray-2
hover:cursor-pointer
text="hover:hover"
title="返回上一页"
@click="back"
>
🏠
</div>
<div text-sm px-2 hover:rounded-md hover:bg-gray-2 hover:cursor-pointer text="hover:hover" @click="onClickAbout">关于</div>
</div>
<div
relative
h="30px"
leading="29px"
pr="137px"
:style="{ paddingRight: isFullScreen ? '0' : '' }"
select-none
border-b="1px solid #E5E5E5"
bg="#F8F8F8"
>
<div absolute top-0 right-0 bottom-0 left-0 style="-webkit-app-region: drag"></div>
<div h-full px-2 flex items-center gap-1 justify-between>
<div flex items-center gap-1>
<img w="16px" h="16px" :src="icon" />
<div relative h-full inline-flex items-center text-sm>{{ config.app_title }}</div>
<div relative class="list">
<div class="item" @click="onClickMenu">{{ t("caidan") }}</div>
</div>
</div>
<div float-right h-full flex items-center relative style="-webkit-app-region: no-drag">
<div
v-if="!isHome"
text-sm
px-2
hover:rounded-md
hover:bg-gray-2
hover:cursor-pointer
text="hover:hover"
title="返回上一页"
@click="back"
>
🏠
</div>
<div text-sm px-2 hover:rounded-md hover:bg-gray-2 hover:cursor-pointer text="hover:hover" @click="onClickAbout">关于</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import icon from "@res/icon.png"
import config from "config"
import { PopupMenu } from "@/bridge/PopupMenu"
import { usePlatForm } from "common/usePlatform"
import icon from "@res/icon.png"
import config from "config"
import { PopupMenu } from "@/bridge/PopupMenu"
import { usePlatForm } from "common/usePlatform"
const { PlatForm } = usePlatForm()
const { PlatForm } = usePlatForm()
const router = useRouter()
const route = useRoute()
const isFullScreen = ref(false)
const router = useRouter()
const route = useRoute()
const isFullScreen = ref(false)
onBeforeMount(async () => {
onBeforeMount(async () => {
isFullScreen.value = await PlatForm.isFullScreen()
})
})
const isHome = computed(() => {
const isHome = computed(() => {
if (route.fullPath === "/") {
return true
return true
}
return false
})
})
function back() {
function back() {
router.push("/")
}
const { t } = useI18n()
const onClickMenu = e => {
}
const { t } = useI18n()
const onClickMenu = e => {
const menu = new PopupMenu([
{
label: isFullScreen.value ? t("qu-xiao-quan-ping") : t("quan-ping"),
async click() {
await PlatForm.toggleFullScreen()
isFullScreen.value = !isFullScreen.value
},
{
label: isFullScreen.value ? t("qu-xiao-quan-ping") : t("quan-ping"),
async click() {
await PlatForm.toggleFullScreen()
isFullScreen.value = !isFullScreen.value
},
{
label: t("qie-huan-kai-fa-zhe-gong-ju"),
async click() {
PlatForm.toggleDevTools()
},
},
{
label: t("qie-huan-kai-fa-zhe-gong-ju"),
async click() {
PlatForm.toggleDevTools()
},
},
])
const obj = e.target.getBoundingClientRect()
menu.show({ x: ~~obj.x, y: ~~(obj.y + obj.height) })
}
}
const onClickAbout = () => {
const onClickAbout = () => {
PlatForm.showAbout()
}
}
</script>
<style lang="scss" scoped>
.list {
.list {
@apply: flex gap="5px";
-webkit-app-region: no-drag;
.item {
@apply: text-sm px-2 hover:rounded-md hover:bg-gray-2 hover:cursor-pointer text="hover:hover";
@apply: text-sm px-2 hover:rounded-md hover:bg-gray-2 hover:cursor-pointer text="hover:hover";
}
}
}
</style>

14
src/renderer/src/components/Versions.vue

@ -1,13 +1,13 @@
<script setup lang="ts">
import { reactive } from "vue"
import { reactive } from "vue"
const versions = reactive({ ...window.electron.process.versions })
const versions = reactive({ ...window.electron.process.versions })
</script>
<template>
<ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li>
<li class="chrome-version">Chromium v{{ versions.chrome }}</li>
<li class="node-version">Node v{{ versions.node }}</li>
</ul>
<ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li>
<li class="chrome-version">Chromium v{{ versions.chrome }}</li>
<li class="node-version">Node v{{ versions.node }}</li>
</ul>
</template>

2
src/renderer/src/composables/useTest.ts

@ -1,3 +1,3 @@
export function useTest() {
console.log("test")
console.log("test")
}

8
src/renderer/src/env.d.ts

@ -2,8 +2,8 @@
/// <reference types="unplugin-vue-router/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue"
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
import type { DefineComponent } from "vue"
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

14
src/renderer/src/i18n/index.ts

@ -13,13 +13,13 @@ import { datetimeFormats } from "locales" // 引入以便热更新同时提供da
console.log(messages)
const i18n = createI18n({
legacy: false,
allowComposition: true,
locale: "zh",
fallbackLocale: "zh",
messages: messages,
// @ts-ignore ...
datetimeFormats,
legacy: false,
allowComposition: true,
locale: "zh",
fallbackLocale: "zh",
messages: messages,
// @ts-ignore ...
datetimeFormats,
})
export { i18n }

12
src/renderer/src/layouts/default.vue

@ -1,15 +1,15 @@
<script lang="ts" setup>
import Simplebar from "simplebar-vue"
import Simplebar from "simplebar-vue"
</script>
<template>
<Simplebar h-full>
<RouterView></RouterView>
</Simplebar>
<Simplebar h-full>
<RouterView></RouterView>
</Simplebar>
</template>
<style scoped>
:deep(.simplebar-content) {
:deep(.simplebar-content) {
height: 100%;
}
}
</style>

8
src/renderer/src/main.ts

@ -13,14 +13,14 @@ const app = createApp(App)
// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
console.error("应用错误:", err)
console.info("错误信息:", info)
// 可以添加错误上报逻辑
console.error("应用错误:", err)
console.info("错误信息:", info)
// 可以添加错误上报逻辑
}
// 开发环境下的性能监控
if (import.meta.env.DEV) {
app.config.performance = true
app.config.performance = true
}
app.use(i18n)

2
src/renderer/src/pages/[...all].vue

@ -1,3 +1,3 @@
<template>
<div @click="$router.back()">Not Found</div>
<div @click="$router.back()">Not Found</div>
</template>

14
src/renderer/src/pages/_ui/App.vue

@ -1,14 +1,14 @@
<script setup lang="ts">
defineOptions({
defineOptions({
title: "观山",
bg: "ty",
})
})
</script>
<template>
<div>
<input type="text" />
<input type="text" />
<input type="text" />
</div>
<div>
<input type="text" />
<input type="text" />
<input type="text" />
</div>
</template>

404
src/renderer/src/pages/_ui/Browser.vue

@ -1,141 +1,141 @@
<script setup lang="ts">
import { onBeforeMount, onBeforeUnmount, onMounted, ref, useTemplateRef, nextTick } from "vue"
import { PopupMenu } from "@/bridge/PopupMenu"
import { onBeforeMount, onBeforeUnmount, onMounted, ref, useTemplateRef, nextTick } from "vue"
import { PopupMenu } from "@/bridge/PopupMenu"
defineOptions({
defineOptions({
title: "浏览器",
index: 2,
bg: "bg",
})
// const PlaceHolderRef = useTemplateRef("PlaceHolder")
// function OnResize() {
// const el = PlaceHolderRef.value
// if (el) {
// const rect = el.getBoundingClientRect().toJSON()
// console.log(rect)
// api.call("TabsCommand.bindElement", rect)
// }
// }
// onMounted(OnResize)
// window.addEventListener("resize", OnResize)
// onBeforeUnmount(() => {
// window.removeEventListener("resize", OnResize)
// })
const PlaceHolder = useTemplateRef("PlaceHolder")
const { stop } = useResizeObserver(PlaceHolder, () => {
})
// const PlaceHolderRef = useTemplateRef("PlaceHolder")
// function OnResize() {
// const el = PlaceHolderRef.value
// if (el) {
// const rect = el.getBoundingClientRect().toJSON()
// console.log(rect)
// api.call("TabsCommand.bindElement", rect)
// }
// }
// onMounted(OnResize)
// window.addEventListener("resize", OnResize)
// onBeforeUnmount(() => {
// window.removeEventListener("resize", OnResize)
// })
const PlaceHolder = useTemplateRef("PlaceHolder")
const { stop } = useResizeObserver(PlaceHolder, () => {
const el = PlaceHolder.value
if (el) {
const rect = el.getBoundingClientRect().toJSON()
api.call("TabsCommand.bindElement", rect)
const rect = el.getBoundingClientRect().toJSON()
api.call("TabsCommand.bindElement", rect)
}
})
})
onBeforeUnmount(() => {
onBeforeUnmount(() => {
stop()
api.call("TabsCommand.closeAll")
})
})
const list = ref<any[]>([])
const curUrl = ref<any>("")
const curIndex = ref<any>(-1)
const listener = (_, v) => {
const list = ref<any[]>([])
const curUrl = ref<any>("")
const curIndex = ref<any>(-1)
const listener = (_, v) => {
list.value = v
const el = v.find(v => v.isActive)
curIndex.value = v.findIndex(v => v.isActive)
if (el) {
curUrl.value = el.showUrl
curUrl.value = el.showUrl
} else {
curUrl.value = ""
curUrl.value = ""
}
}
if (import.meta.hot) {
}
if (import.meta.hot) {
api.off("main:TabsCommand.update", listener)
}
api.on("main:TabsCommand.update", listener)
onMounted(() => {
}
api.on("main:TabsCommand.update", listener)
onMounted(() => {
api.call("TabsCommand.sync")
})
})
onBeforeMount(async () => {
onBeforeMount(async () => {
list.value = await fetch("api://fuck/TabsService/getAllTabs").then(async res => await res.json())
})
})
// const url = ref("")
// const url = ref("")
// async function addTab() {
// if (!url.value) url.value = "about:blank"
// await fetch("api://fuck/TabsService/add", {
// method: "POST",
// body: JSON.stringify({ url: url.value }),
// })
// url.value = ""
// onClick()
// }
// async function addTab() {
// if (!url.value) url.value = "about:blank"
// await fetch("api://fuck/TabsService/add", {
// method: "POST",
// body: JSON.stringify({ url: url.value }),
// })
// url.value = ""
// onClick()
// }
function handleTabContextMenu(_, index) {
function handleTabContextMenu(_, index) {
const menu = new PopupMenu([
{
label: "右侧关闭",
click() {
const all: number[] = []
list.value.forEach((_, i) => {
if (i <= index) return
all.push(i)
})
fetch("api://fuck/TabsService/closeTabAll", {
method: "POST",
body: JSON.stringify({ active: all }),
})
},
},
{
type: "separator",
{
label: "右侧关闭",
click() {
const all: number[] = []
list.value.forEach((_, i) => {
if (i <= index) return
all.push(i)
})
fetch("api://fuck/TabsService/closeTabAll", {
method: "POST",
body: JSON.stringify({ active: all }),
})
},
},
{
type: "separator",
},
])
menu.show()
}
}
function scrollTabIntoView(index: number) {
function scrollTabIntoView(index: number) {
nextTick(() => {
const tabList = document.querySelector(".tab-list")
const tabItems = tabList?.querySelectorAll(".tab-item")
if (tabList && tabItems && tabItems[index]) {
tabItems[index].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" })
}
const tabList = document.querySelector(".tab-list")
const tabItems = tabList?.querySelectorAll(".tab-item")
if (tabList && tabItems && tabItems[index]) {
tabItems[index].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" })
}
})
}
}
async function changeTab(_, index) {
async function changeTab(_, index) {
await api.call("TabsCommand.setActive", index)
scrollTabIntoView(index)
}
}
function addTabInput() {
function addTabInput() {
if (curUrl.value) {
if (curIndex.value !== undefined && curIndex.value >= 0) {
api.call("TabsCommand.nagivate", curIndex.value, curUrl.value)
} else {
api.call("TabsCommand.add", curUrl.value)
}
if (curIndex.value !== undefined && curIndex.value >= 0) {
api.call("TabsCommand.nagivate", curIndex.value, curUrl.value)
} else {
api.call("TabsCommand.add", curUrl.value)
}
}
}
}
async function addTab() {
async function addTab() {
await api.call("TabsCommand.add", "about:blank")
scrollTabIntoView(list.value.length - 1)
}
}
async function closeTab(_, index) {
async function closeTab(_, index) {
await fetch("api://fuck/TabsService/closeTab", {
method: "POST",
body: JSON.stringify({ active: index }),
method: "POST",
body: JSON.stringify({ active: index }),
})
onClick()
}
}
const onClick = async () => {
const onClick = async () => {
list.value = await api.call("TabsCommand.getAllTabs")
// list.value = await fetch("api://fuck/TabsService/getAllTabs").then(async res => await res.json())
// fetch("api://fuck/BasicService/showAbout").then(async res => console.log(await res.json()))
@ -143,85 +143,85 @@ const onClick = async () => {
// method: "POST",
// body: JSON.stringify({ a: "234" }),
// }).then(async res => console.log(await res.json()))
}
}
function onClickDevTool() {
function onClickDevTool() {
fetch("api://fuck/BasicService/openTabDevtool")
}
}
</script>
<template>
<div h="100px" flex flex-col b-b="1px solid var(--border-color)" class="tab-container">
<!-- Tab列表 -->
<div class="tab-list-container">
<div class="tab-list">
<div
v-for="(item, index) in list"
:key="index"
:class="{
'tab-item': true,
active: item.isActive,
}"
@contextmenu="handleTabContextMenu(item, index)"
@click="changeTab(item, index)"
>
<div class="tab-content">
<!-- 网站图标 -->
<img v-if="item.favicons?.length" :src="item.favicons[0]" class="tab-icon" alt="" />
<div v-else class="tab-icon-placeholder"></div>
<!-- 标题 -->
<div class="tab-title">{{ item.title || "加载中..." }}</div>
<!-- 关闭按钮 -->
<div class="tab-close" @click.stop="closeTab(item, index)">
<svg width="16" height="16" viewBox="0 0 16 16">
<path
d="M12.81 4.36l-1.17-1.17L8 6.83 4.36 3.19 3.19 4.36 6.83 8l-3.64 3.64 1.17 1.17L8 9.17l3.64 3.64 1.17-1.17L9.17 8z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
</div>
<!-- 新建标签页按钮移到容器外部 -->
<div class="new-tab-button-container">
<div class="new-tab-button" @click.stop="addTab()">
<svg width="20" height="20" viewBox="0 0 20 20">
<path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</div>
<div h="100px" flex flex-col b-b="1px solid var(--border-color)" class="tab-container">
<!-- Tab列表 -->
<div class="tab-list-container">
<div class="tab-list">
<div
v-for="(item, index) in list"
:key="index"
:class="{
'tab-item': true,
active: item.isActive,
}"
@contextmenu="handleTabContextMenu(item, index)"
@click="changeTab(item, index)"
>
<div class="tab-content">
<!-- 网站图标 -->
<img v-if="item.favicons?.length" :src="item.favicons[0]" class="tab-icon" alt="" />
<div v-else class="tab-icon-placeholder"></div>
<!-- 标题 -->
<div class="tab-title">{{ item.title || "加载中..." }}</div>
<!-- 关闭按钮 -->
<div class="tab-close" @click.stop="closeTab(item, index)">
<svg width="16" height="16" viewBox="0 0 16 16">
<path
d="M12.81 4.36l-1.17-1.17L8 6.83 4.36 3.19 3.19 4.36 6.83 8l-3.64 3.64 1.17 1.17L8 9.17l3.64 3.64 1.17-1.17L9.17 8z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
<!-- 地址栏 -->
<div class="address-bar">
<div class="url-input-container">
<input v-model="curUrl" placeholder="输入网址" type="text" class="url-input" />
</div>
<button class="action-button" @click="addTabInput()">前往</button>
<button class="action-button" @click="onClickDevTool()">DevTool</button>
</div>
<!-- 新建标签页按钮移到容器外部 -->
<div class="new-tab-button-container">
<div class="new-tab-button" @click.stop="addTab()">
<svg width="20" height="20" viewBox="0 0 20 20">
<path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</div>
</div>
</div>
<!-- 内容区域 -->
<div ref="PlaceHolder" ml="1px" flex-1 h-0 flex items-center justify-center>
<!-- 保持原有内容 -->
<!-- 地址栏 -->
<div class="address-bar">
<div class="url-input-container">
<input v-model="curUrl" placeholder="输入网址" type="text" class="url-input" />
</div>
<button class="action-button" @click="addTabInput()">前往</button>
<button class="action-button" @click="onClickDevTool()">DevTool</button>
</div>
</div>
<!-- 内容区域 -->
<div ref="PlaceHolder" ml="1px" flex-1 h-0 flex items-center justify-center>
<!-- 保持原有内容 -->
</div>
</template>
<style scoped>
.tab-container {
.tab-container {
background: var(--tab-bar-bg, #f3f3f3);
border-bottom: none;
padding-top: 4px;
height: auto;
min-height: 80px;
}
}
.tab-list-container {
.tab-list-container {
position: relative;
display: flex;
align-items: flex-end;
@ -230,9 +230,9 @@ function onClickDevTool() {
margin-bottom: 0;
width: 100%;
overflow: auto;
}
}
.tab-list {
.tab-list {
height: 32px;
display: flex;
align-items: flex-end;
@ -245,11 +245,11 @@ function onClickDevTool() {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
&::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
display: none; /* Chrome, Safari and Opera */
}
}
}
.tab-item {
.tab-item {
position: relative;
min-width: 160px;
max-width: 240px;
@ -263,9 +263,9 @@ function onClickDevTool() {
align-items: center;
flex-shrink: 0; /* 防止标签被压缩 */
cursor: pointer;
}
}
.tab-item::after {
.tab-item::after {
content: "";
position: absolute;
right: 0;
@ -274,20 +274,20 @@ function onClickDevTool() {
width: 1px;
background: var(--tab-separator-color, #bdc1c6);
opacity: 0.3;
}
}
.tab-item:hover {
.tab-item:hover {
background: var(--tab-hover-bg, #e9ebee);
}
}
.tab-item.active {
.tab-item.active {
background: var(--tab-active-bg, #fff);
z-index: 2;
height: 32px;
margin-bottom: -1px;
}
}
.tab-item.active::before {
.tab-item.active::before {
content: "";
position: absolute;
left: 0;
@ -296,34 +296,34 @@ function onClickDevTool() {
height: 2px;
background: var(--primary-color, #1a73e8);
border-radius: 2px 2px 0 0;
}
}
.tab-content {
.tab-content {
display: flex;
align-items: center;
padding: 0 8px;
height: 100%;
gap: 6px;
width: 100%;
}
}
.tab-icon {
.tab-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
border-radius: 3px;
}
}
.tab-icon-placeholder {
.tab-icon-placeholder {
width: 16px;
height: 16px;
background: #bdc1c6;
border-radius: 50%;
flex-shrink: 0;
opacity: 0.7;
}
}
.tab-title {
.tab-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
@ -332,13 +332,13 @@ function onClickDevTool() {
color: var(--text-color, #5f6368);
line-height: 1.2;
padding-right: 4px;
}
}
.tab-item.active .tab-title {
.tab-item.active .tab-title {
color: var(--active-text-color, #202124);
}
}
.tab-close {
.tab-close {
width: 16px;
height: 16px;
display: flex;
@ -349,27 +349,27 @@ function onClickDevTool() {
opacity: 0;
transition: opacity 0.15s ease;
flex-shrink: 0;
}
}
.tab-item:hover .tab-close {
.tab-item:hover .tab-close {
opacity: 0.6;
}
}
.tab-close:hover {
.tab-close:hover {
background: var(--close-hover-bg, rgba(0, 0, 0, 0.08));
opacity: 1;
}
}
.new-tab-button-container {
.new-tab-button-container {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 32px;
background: var(--tab-bar-bg);
}
}
.new-tab-button {
.new-tab-button {
width: 32px;
height: 32px;
display: flex;
@ -381,14 +381,14 @@ function onClickDevTool() {
cursor: pointer;
opacity: 0.8;
background: transparent;
}
}
.new-tab-button:hover {
.new-tab-button:hover {
background: var(--tab-hover-bg, rgba(0, 0, 0, 0.06));
opacity: 1;
}
}
.address-bar {
.address-bar {
padding: 8px 12px;
margin: 0;
background: var(--address-bar-bg, #fff);
@ -397,9 +397,9 @@ function onClickDevTool() {
gap: 8px;
width: 100%;
border-top: 1px solid var(--tab-separator-color, rgba(0, 0, 0, 0.1));
}
}
.url-input-container {
.url-input-container {
flex: 1;
height: 36px;
background: var(--input-bg, #f1f3f4);
@ -409,9 +409,9 @@ function onClickDevTool() {
align-items: center;
margin: 0;
min-width: 0;
}
}
.url-input {
.url-input {
width: 100%;
height: 100%;
border: none;
@ -419,9 +419,9 @@ function onClickDevTool() {
outline: none;
font-size: 14px;
color: var(--text-color, #333);
}
}
.action-button {
.action-button {
flex-shrink: 0;
height: 36px;
padding: 0 16px;
@ -432,14 +432,14 @@ function onClickDevTool() {
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
}
.action-button:hover {
.action-button:hover {
background: var(--button-hover-bg, #f1f3f4);
}
}
/* 修改CSS变量 */
:root {
/* 修改CSS变量 */
:root {
--tab-bar-bg: #f1f3f4;
--tab-bg: rgba(32, 33, 36, 0.1);
--tab-hover-bg: rgba(32, 33, 36, 0.08);
@ -451,9 +451,9 @@ function onClickDevTool() {
--close-hover-bg: rgba(0, 0, 0, 0.08);
--primary-color: #1a73e8;
--new-tab-shadow: 0 -8px 12px -6px rgba(0, 0, 0, 0.1);
}
}
[data-theme="dark"] {
[data-theme="dark"] {
--tab-bar-bg: #202124;
--tab-bg: rgba(255, 255, 255, 0.1);
--tab-hover-bg: rgba(255, 255, 255, 0.08);
@ -465,5 +465,5 @@ function onClickDevTool() {
--close-hover-bg: rgba(255, 255, 255, 0.08);
--primary-color: #8ab4f8;
--new-tab-shadow: 0 -8px 12px -6px rgba(0, 0, 0, 0.3);
}
}
</style>

58
src/renderer/src/pages/about/index.vue

@ -1,37 +1,37 @@
<script setup lang="ts">
definePage({
definePage({
name: "about",
meta: {
title: "听雨",
bg: "gs",
title: "听雨",
bg: "gs",
},
})
})
// const activeTab = ref(0)
// const TopMenu = computed<any[]>(() => {
// return [
// { key: 0, title: "sada", url: "/setting" },
// { key: 1, title: "sdas", url: "/setting/editor" },
// { key: 2, title: "asdas", url: "/setting/update" },
// ]
// })
// const route = useRoute()
// watch(
// () => route,
// route => {
// for (let i = 0; i < TopMenu.value.length; i++) {
// const element = TopMenu.value[i]
// if (route.path.startsWith(element.url)) {
// activeTab.value = element.key
// }
// }
// },
// { immediate: true },
// )
// const activeTab = ref(0)
// const TopMenu = computed<any[]>(() => {
// return [
// { key: 0, title: "sada", url: "/setting" },
// { key: 1, title: "sdas", url: "/setting/editor" },
// { key: 2, title: "asdas", url: "/setting/update" },
// ]
// })
// const route = useRoute()
// watch(
// () => route,
// route => {
// for (let i = 0; i < TopMenu.value.length; i++) {
// const element = TopMenu.value[i]
// if (route.path.startsWith(element.url)) {
// activeTab.value = element.key
// }
// }
// },
// { immediate: true },
// )
</script>
<template>
<div>
about
<!-- <HTab v-model="activeTab" :list="TopMenu"></HTab> -->
</div>
<div>
about
<!-- <HTab v-model="activeTab" :list="TopMenu"></HTab> -->
</div>
</template>

218
src/renderer/src/pages/browser.vue

@ -1,143 +1,143 @@
<script setup lang="ts">
import Simplebar from "simplebar-vue"
import { getAssetsFile } from "@/utils"
import Simplebar from "simplebar-vue"
import { getAssetsFile } from "@/utils"
const allModules: Record<string, any> = import.meta.glob("./_ui/**/*.vue", { eager: true })
let allApp: any[] = []
Object.keys(allModules).forEach(key => {
const allModules: Record<string, any> = import.meta.glob("./_ui/**/*.vue", { eager: true })
let allApp: any[] = []
Object.keys(allModules).forEach(key => {
// let [, p] = key.match("./_ui/(.*?).vue")!
// p = p.replace(/\.vue$/, "")
const m = allModules[key]?.default || allModules[key]
allApp.push({
label: m.title,
bg: m.bg,
_sort: m.index ?? 0,
comp: m,
label: m.title,
bg: m.bg,
_sort: m.index ?? 0,
comp: m,
})
})
allApp = allApp.sort((a, b) => (a.index - b.index <= 0 ? 1 : -1))
})
allApp = allApp.sort((a, b) => (a.index - b.index <= 0 ? 1 : -1))
const active = ref(0)
// const allApp = [
// { label: "", comp: defineAsyncComponent(() => import("./_ui/Browser.vue")) },
// { label: "", bg: "gs", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", bg: "ty", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "访", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// ]
const active = ref(0)
// const allApp = [
// { label: "", comp: defineAsyncComponent(() => import("./_ui/Browser.vue")) },
// { label: "", bg: "gs", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", bg: "ty", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "访", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// { label: "", comp: defineAsyncComponent(() => import("./_ui/App.vue")) },
// ]
const activeBg = computed(() => {
const activeBg = computed(() => {
if (active.value === undefined) return ""
const value = allApp[active.value]?.bg
return value ? getAssetsFile(`@/assets/images/home/${value}.png`) : ""
})
})
function onClick(index: number) {
function onClick(index: number) {
active.value = index
}
}
</script>
<template>
<div h-full flex>
<div w="100px" h-full relative max-w="200px" min-w="80px">
<Simplebar h-full>
<div
v-for="(app, index) in allApp"
:key="index"
p="8px 10px"
text="12px"
border
border-b
h="30px"
cursor="pointer"
hover:bg-gray-50
class="item"
transition-all
:class="{ active: active === index }"
@click="onClick(index)"
>
<div class="text" transition-all position="absolute" left="10px">{{ app.label }}</div>
</div>
</Simplebar>
<!-- <AdjustLine></AdjustLine> -->
</div>
<div class="content" relative b-l="1px solid #E5E5E5" flex-1 w-0 overflow-auto flex flex-col>
<div v-if="activeBg" class="bg" :style="{ backgroundImage: activeBg ? `url(${activeBg})` : '' }"></div>
<div @click="$router.push('/about')">关于</div>
<component :is="allApp[active].comp" v-if="allApp[active]"></component>
<div h-full flex>
<div w="100px" h-full relative max-w="200px" min-w="80px">
<Simplebar h-full>
<div
v-for="(app, index) in allApp"
:key="index"
p="8px 10px"
text="12px"
border
border-b
h="30px"
cursor="pointer"
hover:bg-gray-50
class="item"
transition-all
:class="{ active: active === index }"
@click="onClick(index)"
>
<div class="text" transition-all position="absolute" left="10px">{{ app.label }}</div>
</div>
</Simplebar>
<!-- <AdjustLine></AdjustLine> -->
</div>
<div class="content" relative b-l="1px solid #E5E5E5" flex-1 w-0 overflow-auto flex flex-col>
<div v-if="activeBg" class="bg" :style="{ backgroundImage: activeBg ? `url(${activeBg})` : '' }"></div>
<div @click="$router.push('/about')">关于</div>
<component :is="allApp[active].comp" v-if="allApp[active]"></component>
</div>
</div>
</template>
<style lang="scss" scoped>
.content {
.content {
.bg {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
opacity: 0.1;
// blur(4px)
filter: brightness(1);
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
opacity: 0.1;
// blur(4px)
filter: brightness(1);
}
}
.item {
}
.item {
position: relative;
&::before {
content: "";
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 6px;
background-color: #f3f4f6;
transition: all linear 300ms;
content: "";
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 6px;
background-color: #f3f4f6;
transition: all linear 300ms;
}
&:hover {
&::before {
width: 30px;
}
.text {
left: 20px;
}
&::before {
width: 30px;
}
.text {
left: 20px;
}
}
&.active {
@apply: text-black;
&::before {
width: 100%;
}
.text {
left: 50%;
transform: translateX(-50%);
}
@apply: text-black;
&::before {
width: 100%;
}
.text {
left: 50%;
transform: translateX(-50%);
}
}
.text {
transition-duration: 300ms;
transition-duration: 300ms;
}
}
}
</style>

42
src/renderer/src/pages/index.vue

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

6
src/renderer/src/router/index.ts

@ -5,8 +5,8 @@ import { setupLayouts } from "virtual:generated-layouts"
const routes = setupLayouts(generatedRoutes)
const router = createRouter({
history: createWebHashHistory(),
routes,
history: createWebHashHistory(),
routes,
})
export { router }
@ -14,5 +14,5 @@ export { router }
export default router
if (import.meta.hot) {
handleHotUpdate(router)
handleHotUpdate(router)
}

2
src/renderer/src/shims.d.ts

@ -1,5 +1,5 @@
import type { AttributifyAttributes } from "@unocss/preset-attributify"
declare module "@vue/runtime-dom" {
interface HTMLAttributes extends AttributifyAttributes {}
interface HTMLAttributes extends AttributifyAttributes {}
}

8
src/renderer/src/utils/index.ts

@ -4,8 +4,8 @@
* @returns
*/
export const getAssetsFile = url => {
const urlArr = String(url).split("/")
const prefix = urlArr.slice(-2)[0]
const fileName = urlArr.slice(-1)[0]
return new URL(`../assets/images/${prefix}/${fileName}`, import.meta.url).href
const urlArr = String(url).split("/")
const prefix = urlArr.slice(-2)[0]
const fileName = urlArr.slice(-1)[0]
return new URL(`../assets/images/${prefix}/${fileName}`, import.meta.url).href
}

25
src/types/global.d.ts

@ -1,21 +1,20 @@
type Api = {
call: (command: string, ...args: any[]) => Promise<any>
callLong: (command: string, ...args: any[]) => Promise<any>
callSync: (command: string, ...args: any[]) => any
send: (command: string, ...argu: any[]) => any
sendSync: (command: string, ...argu: any[]) => any
on: <T extends string>(command: T, cb: (event: IpcRendererEvent, ...args: any[]) => void) => () => void
once: (command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) => () => void
off: (command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) => void
offAll: (command: string) => void
popupMenu: (options: IPopupMenuOption) => void
call: (command: string, ...args: any[]) => Promise<any>
callLong: (command: string, ...args: any[]) => Promise<any>
callSync: (command: string, ...args: any[]) => any
send: (command: string, ...argu: any[]) => any
sendSync: (command: string, ...argu: any[]) => any
on: <T extends string>(command: T, cb: (event: IpcRendererEvent, ...args: any[]) => void) => () => void
once: (command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) => () => void
off: (command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) => void
offAll: (command: string) => void
popupMenu: (options: IPopupMenuOption) => void
}
declare const electron: typeof import("@electron-toolkit/preload").electronAPI
declare const api: Api
interface Window {
electron: typeof import("@electron-toolkit/preload").electronAPI
api: Api
electron: typeof import("@electron-toolkit/preload").electronAPI
api: Api
}

10
src/types/popup-menu.ts

@ -1,12 +1,12 @@
import type { PopupOptions } from "electron"
export interface IMenuItemOption extends Electron.MenuItemConstructorOptions {
// 参见:https://www.electronjs.org/docs/api/menu-item
_click_evt?: string
// 参见:https://www.electronjs.org/docs/api/menu-item
_click_evt?: string
}
export interface IPopupMenuOption {
menu_id: string
items: IMenuItemOption[]
popupOptions?: PopupOptions
menu_id: string
items: IMenuItemOption[]
popupOptions?: PopupOptions
}

136
uno.config.ts

@ -2,75 +2,75 @@ import { defineConfig, presetAttributify, presetUno, transformerDirectives } fro
import presetRemToPx from "@unocss/preset-rem-to-px"
export default defineConfig({
presets: [presetAttributify(), presetUno(), presetRemToPx()],
transformers: [transformerDirectives()],
shortcuts: [
// 正方形 square-100px
[
/^square-\[?(.*?)\]?$/,
([, size]) => {
return `w-${size} h-${size}`
},
],
// 圆形 circle-100px
[
/^circle-\[?(.*?)\]?$/,
([, size]) => {
return `square-${size} rounded-full`
},
],
// 垂直水平居中
["flex-center", "flex justify-center items-center"],
presets: [presetAttributify(), presetUno(), presetRemToPx()],
transformers: [transformerDirectives()],
shortcuts: [
// 正方形 square-100px
[
/^square-\[?(.*?)\]?$/,
([, size]) => {
return `w-${size} h-${size}`
},
],
rules: [
[
/^text-(.*)$/,
([, c]) => {
if (c === "normal") return { color: "var(--text-normal)" }
if (c === "hover") return { color: "var(--text-hover)" }
},
],
// 多行文本超出部分省略号 line-n (已内置 line-clamp-n)
[
/^line-(\d+)$/,
([, l]) => {
if (~~l === 1) {
return {
overflow: "hidden",
"text-overflow": "ellipsis",
"white-space": "nowrap",
width: "100%",
}
}
return {
overflow: "hidden",
display: "-webkit-box",
"-webkit-box-orient": "vertical",
"-webkit-line-clamp": l,
}
},
],
// 一侧圆角 rounded-left-5px (已内置 rounded-l-n)
[
/^rounded-(left|right|top|bottom)-(.*?)$/,
([, position, m]) => {
let x1, x2, y1, y2
if (["left", "right"].includes(position)) {
y1 = "top"
y2 = "bottom"
x1 = x2 = position
} else {
x1 = "left"
x2 = "right"
y1 = y2 = position
}
if (m === "full") m = "99999px"
// 圆形 circle-100px
[
/^circle-\[?(.*?)\]?$/,
([, size]) => {
return `square-${size} rounded-full`
},
],
// 垂直水平居中
["flex-center", "flex justify-center items-center"],
],
rules: [
[
/^text-(.*)$/,
([, c]) => {
if (c === "normal") return { color: "var(--text-normal)" }
if (c === "hover") return { color: "var(--text-hover)" }
},
],
// 多行文本超出部分省略号 line-n (已内置 line-clamp-n)
[
/^line-(\d+)$/,
([, l]) => {
if (~~l === 1) {
return {
overflow: "hidden",
"text-overflow": "ellipsis",
"white-space": "nowrap",
width: "100%",
}
}
return {
overflow: "hidden",
display: "-webkit-box",
"-webkit-box-orient": "vertical",
"-webkit-line-clamp": l,
}
},
],
// 一侧圆角 rounded-left-5px (已内置 rounded-l-n)
[
/^rounded-(left|right|top|bottom)-(.*?)$/,
([, position, m]) => {
let x1, x2, y1, y2
if (["left", "right"].includes(position)) {
y1 = "top"
y2 = "bottom"
x1 = x2 = position
} else {
x1 = "left"
x2 = "right"
y1 = y2 = position
}
if (m === "full") m = "99999px"
return {
[`border-${y1}-${x1}-radius`]: m,
[`border-${y2}-${x2}-radius`]: m,
}
},
],
return {
[`border-${y1}-${x1}-radius`]: m,
[`border-${y2}-${x2}-radius`]: m,
}
},
],
],
})

2
vue-macros.config.ts

@ -1,4 +1,4 @@
import { defineConfig } from "unplugin-vue-macros"
export default defineConfig({
// 选项
// 选项
})

4
推荐.md

@ -1,15 +1,11 @@
插件化:
https://rubickcenter.github.io/docs/core/index.html#%E5%9F%BA%E4%BA%8E-browserview-%E5%AE%9E%E7%8E%B0%E6%8F%92%E4%BB%B6%E5%8C%96%E8%83%BD%E5%8A%9B
electron+vue虚拟桌面开发遇坑之透明窗口鼠标穿透
https://blog.csdn.net/weixin_42421494/article/details/102800491
截图
https://zhuanlan.zhihu.com/p/46043613?from_voters_page=true

Loading…
Cancel
Save