Browse Source

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

此次提交主要将代码中的缩进统一调整为2个空格,替换原有的4个空格或Tab缩进。这一改动不影响代码功能,但有助于提升代码的一致性和可读性。
feat/icon
npmrun 3 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. 1175
      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. 441
      src/renderer/src/components/CodeEditor/code-editor.vue
  62. 58
      src/renderer/src/components/CodeEditor/utils.ts
  63. 138
      src/renderer/src/components/NavBar.vue
  64. 14
      src/renderer/src/components/Versions.vue
  65. 2
      src/renderer/src/composables/useTest.ts
  66. 8
      src/renderer/src/env.d.ts
  67. 14
      src/renderer/src/i18n/index.ts
  68. 12
      src/renderer/src/layouts/default.vue
  69. 8
      src/renderer/src/main.ts
  70. 2
      src/renderer/src/pages/[...all].vue
  71. 14
      src/renderer/src/pages/_ui/App.vue
  72. 404
      src/renderer/src/pages/_ui/Browser.vue
  73. 58
      src/renderer/src/pages/about/index.vue
  74. 218
      src/renderer/src/pages/browser.vue
  75. 42
      src/renderer/src/pages/index.vue
  76. 6
      src/renderer/src/router/index.ts
  77. 2
      src/renderer/src/shims.d.ts
  78. 8
      src/renderer/src/utils/index.ts
  79. 25
      src/types/global.d.ts
  80. 10
      src/types/popup-menu.ts
  81. 136
      uno.config.ts
  82. 2
      vue-macros.config.ts
  83. 4
      推荐.md

60
.prettierrc

@ -1,32 +1,32 @@
{ {
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"semi": false, "semi": false,
"singleQuote": false, "singleQuote": false,
"trailingComma": "all", "trailingComma": "all",
"bracketSpacing": true, "bracketSpacing": true,
"arrowParens": "avoid", "arrowParens": "avoid",
"printWidth": 140, "printWidth": 140,
"htmlWhitespaceSensitivity": "ignore", "htmlWhitespaceSensitivity": "ignore",
"proseWrap": "preserve", "proseWrap": "preserve",
"endOfLine": "auto", "endOfLine": "auto",
"vueIndentScriptAndStyle": true, "vueIndentScriptAndStyle": true,
"embeddedLanguageFormatting": "auto", "embeddedLanguageFormatting": "auto",
"jsxSingleQuote": false, "jsxSingleQuote": false,
"jsxBracketSameLine": false, "jsxBracketSameLine": false,
"quoteProps": "as-needed", "quoteProps": "as-needed",
"overrides": [ "overrides": [
{ {
"files": "*.json", "files": "*.json",
"options": { "options": {
"printWidth": 80 "printWidth": 80
} }
}, },
{ {
"files": ["*.vue", "*.tsx"], "files": ["*.vue", "*.tsx"],
"options": { "options": {
"singleAttributePerLine": false "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", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Debug Main Process", "name": "Debug Main Process",
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": { "windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
}, },
"runtimeArgs": ["--sourcemap"], "runtimeArgs": ["--sourcemap"],
"env": { "env": {
"REMOTE_DEBUGGING_PORT": "9222" "REMOTE_DEBUGGING_PORT": "9222"
} }
}, },
{ {
"name": "Debug Renderer Process", "name": "Debug Renderer Process",
"port": 9222, "port": 9222,
"request": "attach", "request": "attach",
"type": "chrome", "type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer", "webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000, "timeout": 60000,
"presentation": { "presentation": {
"hidden": true "hidden": true
} }
} }
], ],
"compounds": [ "compounds": [
{ {
"name": "Debug All", "name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"], "configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": { "presentation": {
"order": 1 "order": 1
} }
} }
] ]
} }

32
.vscode/settings.json

@ -1,18 +1,18 @@
{ {
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"i18n-ally.localesPaths": ["packages/locales/languages"], "i18n-ally.localesPaths": ["packages/locales/languages"],
"i18n-ally.sourceLanguage": "zh", "i18n-ally.sourceLanguage": "zh",
"i18n-ally.displayLanguage": "zh", "i18n-ally.displayLanguage": "zh",
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.extract.autoDetect": true, "i18n-ally.extract.autoDetect": true,
"i18n-ally.enabledFrameworks": ["vue"], "i18n-ally.enabledFrameworks": ["vue"],
"i18n-ally.enabledParsers": ["json"] "i18n-ally.enabledParsers": ["json"]
} }

54
config/index.ts

@ -7,38 +7,38 @@ type LogoType = "logo" | "bg"
// 配置接口定义 // 配置接口定义
interface IDefaultConfig { interface IDefaultConfig {
language: LanguageType language: LanguageType
"common.theme": ThemeType "common.theme": ThemeType
"desktop:wallpaper": string "desktop:wallpaper": string
"update.repo"?: string "update.repo"?: string
"update.owner"?: string "update.owner"?: string
"update.allowDowngrade": boolean "update.allowDowngrade": boolean
"update.allowPrerelease": boolean "update.allowPrerelease": boolean
"editor.bg": string "editor.bg": string
"editor.logoType": LogoType "editor.logoType": LogoType
"editor.fontFamily": string "editor.fontFamily": string
storagePath: string storagePath: string
} }
interface IConfig { interface IConfig {
app_title: string app_title: string
default_config: IDefaultConfig default_config: IDefaultConfig
} }
// 默认配置导出 // 默认配置导出
export default { export default {
app_title: "zephyr", // 和风 app_title: "zephyr", // 和风
default_config: { default_config: {
storagePath: "$storagePath$", storagePath: "$storagePath$",
language: "zh", language: "zh",
"common.theme": "auto", "common.theme": "auto",
"desktop:wallpaper": "", "desktop:wallpaper": "",
"editor.bg": "", "editor.bg": "",
"editor.logoType": "logo", "editor.logoType": "logo",
"editor.fontFamily": "Cascadia Mono, Consolas, 'Courier New', monospace", "editor.fontFamily": "Cascadia Mono, Consolas, 'Courier New', monospace",
"update.repo": "wood-desktop", "update.repo": "wood-desktop",
"update.owner": "npmrun", "update.owner": "npmrun",
"update.allowDowngrade": false, "update.allowDowngrade": false,
"update.allowPrerelease": false, "update.allowPrerelease": false,
}, },
} as const satisfies IConfig } as const satisfies IConfig

62
electron-builder.yml

@ -1,45 +1,45 @@
appId: com.zephyr.app appId: com.zephyr.app
productName: zephyr productName: zephyr
directories: directories:
buildResources: build buildResources: build
files: files:
- "!**/.vscode/*" - "!**/.vscode/*"
- "!src/*" - "!src/*"
- "!electron.vite.config.{js,ts,mjs,cjs}" - "!electron.vite.config.{js,ts,mjs,cjs}"
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}" - "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}" - "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
- "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}" - "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}"
asarUnpack: asarUnpack:
- resources/** - resources/**
win: win:
executableName: zephyr executableName: zephyr
nsis: nsis:
artifactName: ${name}-${version}-setup.${ext} artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName} shortcutName: ${productName}
uninstallDisplayName: ${productName} uninstallDisplayName: ${productName}
createDesktopShortcut: always createDesktopShortcut: always
mac: mac:
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
extendInfo: extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera. - NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone. - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false notarize: false
dmg: dmg:
artifactName: ${name}-${version}.${ext} artifactName: ${name}-${version}.${ext}
linux: linux:
target: target:
- AppImage - AppImage
- snap - snap
- deb - deb
maintainer: electronjs.org maintainer: electronjs.org
category: Utility category: Utility
appImage: appImage:
artifactName: ${name}-${version}.${ext} artifactName: ${name}-${version}.${ext}
npmRebuild: false npmRebuild: false
publish: publish:
provider: generic provider: generic
url: https://example.com/auto-updates url: https://example.com/auto-updates
electronDownload: 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" import monacoEditorPlugin from "vite-plugin-monaco-editor"
export default defineConfig({ export default defineConfig({
main: { main: {
resolve: { resolve: {
alias: { alias: {
config: resolve("config"), config: resolve("config"),
main: resolve("src/main"), main: resolve("src/main"),
common: resolve("src/common"), common: resolve("src/common"),
"@res": resolve("resources"), "@res": resolve("resources"),
}, },
},
plugins: [externalizeDepsPlugin()],
}, },
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: { css: {
root: resolve(__dirname, "./src/renderer"), preprocessorOptions: {
resolve: { scss: {
alias: { additionalData: `@use "@/assets/style/global" as *;\n`,
config: resolve("config"), api: "modern-compiler",
common: resolve("src/common"),
"@": resolve("src/renderer/src"),
"@res": resolve("resources"),
},
}, },
css: { },
preprocessorOptions: { },
scss: { build: {
additionalData: `@use "@/assets/style/global" as *;\n`, rollupOptions: {
api: "modern-compiler", input: {
}, main: resolve(__dirname, "./src/renderer/index.html"),
}, about: resolve(__dirname, "./src/renderer/about.html"),
}, },
build: { },
rollupOptions: { },
input: { plugins: [
main: resolve(__dirname, "./src/renderer/index.html"), UnoCSS(),
about: resolve(__dirname, "./src/renderer/about.html"), 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(), VueI18nPlugin({
VueMacros({ compositionOnly: false,
plugins: { include: resolve(__dirname, "packages/locales/languages/**"),
vue: vue(), }),
vueJsx: vueJsx(), Layouts({
vueRouter: VueRouter({ layoutsDirs: "src/layouts",
root: resolve(__dirname, "src/renderer"), pagesDirs: "src/pages",
// https://github.com/posva/unplugin-vue-router defaultLayout: "default",
extensions: [".vue", ".setup.tsx"], extensions: ["vue", "setup.tsx"],
exclude: ["**/_ui"], exclude: ["**/_ui"],
}), }),
}, // https://github.com/antfu/unplugin-auto-import
}), AutoImport({
VueI18nPlugin({ imports: [
compositionOnly: false, "vue",
include: resolve(__dirname, "packages/locales/languages/**"), "@vueuse/core",
}), VueRouterAutoImports,
Layouts({ {
layoutsDirs: "src/layouts", // add any other imports you were relying on
pagesDirs: "src/pages", "vue-router/auto": ["useLink"],
defaultLayout: "default", },
extensions: ["vue", "setup.tsx"], "vue-i18n",
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")
},
}),
], ],
}, 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", "name": "zephyr",
"type": "module", "type": "module",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
"description": "An Electron application with Vue and TypeScript", "description": "An Electron application with Vue and TypeScript",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "example.com", "author": "example.com",
"homepage": "https://electron-vite.org", "homepage": "https://electron-vite.org",
"scripts": { "scripts": {
"runInstall": "node node_modules/electron/install.js", "runInstall": "node node_modules/electron/install.js",
"format": "prettier --write .", "format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false", "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web", "typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview", "start": "electron-vite preview",
"dev": "chcp 65001 && set DEBUG=app:*&& electron-vite dev", "dev": "chcp 65001 && set DEBUG=app:*&& electron-vite dev",
"dev:watch": "chcp 65001 & set DEBUG=app:*& electron-vite dev --watch", "dev:watch": "chcp 65001 & set DEBUG=app:*& electron-vite dev --watch",
"build": "npm run typecheck && electron-vite build", "build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir", "build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win", "build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac", "build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux" "build:linux": "npm run build && electron-builder --linux"
}, },
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"inversify": "^6.2.2", "inversify": "^6.2.2",
"lowdb": "^7.0.1", "lowdb": "^7.0.1",
"reflect-metadata": "^0.2.2" "reflect-metadata": "^0.2.2"
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2", "@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-ts": "^2.0.0", "@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@intlify/unplugin-vue-i18n": "^6.0.3", "@intlify/unplugin-vue-i18n": "^6.0.3",
"@rushstack/eslint-patch": "^1.10.5", "@rushstack/eslint-patch": "^1.10.5",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/node": "^20.17.19", "@types/node": "^20.17.19",
"@unocss/preset-rem-to-px": "^0.64.1", "@unocss/preset-rem-to-px": "^0.64.1",
"@unocss/reset": "^0.64.1", "@unocss/reset": "^0.64.1",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
"@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0", "@vue/eslint-config-typescript": "^13.0.0",
"@vueuse/core": "^12.7.0", "@vueuse/core": "^12.7.0",
"debug": "^4.4.0", "debug": "^4.4.0",
"electron": "^31.7.7", "electron": "^31.7.7",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"electron-vite": "^2.3.0", "electron-vite": "^2.3.0",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-plugin-vue": "^9.32.0", "eslint-plugin-vue": "^9.32.0",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"locales": "workspace:*", "locales": "workspace:*",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"prettier": "^3.5.1", "prettier": "^3.5.1",
"rotating-file-stream": "^3.2.6", "rotating-file-stream": "^3.2.6",
"sass": "^1.85.0", "sass": "^1.85.0",
"simplebar-vue": "^2.4.0", "simplebar-vue": "^2.4.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"unocss": "^0.64.1", "unocss": "^0.64.1",
"unplugin-auto-import": "^19.1.0", "unplugin-auto-import": "^19.1.0",
"unplugin-vue-components": "^28.4.0", "unplugin-vue-components": "^28.4.0",
"unplugin-vue-macros": "^2.14.2", "unplugin-vue-macros": "^2.14.2",
"unplugin-vue-router": "^0.11.2", "unplugin-vue-router": "^0.11.2",
"vite": "^5.4.14", "vite": "^5.4.14",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-vue-layouts": "^0.11.0", "vite-plugin-vue-layouts": "^0.11.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-i18n": "^11.1.1", "vue-i18n": "^11.1.1",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"vue-tsc": "^2.1.10" "vue-tsc": "^2.1.10"
} }
} }

2
pnpm-workspace.yaml

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

17
resources/fuck.html

@ -1,11 +1,12 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Document</title>
</head> </head>
<body> <body>
前往 <a href="https://baidu.com" target="_blank">百度</a> 前往
</body> <a href="https://baidu.com" target="_blank">百度</a>
</body>
</html> </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" import TabsCommand from "common/event/Tabs/main/command"
const modules = new ContainerModule(bind => { const modules = new ContainerModule(bind => {
bind("TabsCommand").to(TabsCommand).inSingletonScope() bind("TabsCommand").to(TabsCommand).inSingletonScope()
bind("PlatFormCommand").to(PlatFormCommand).inSingletonScope() bind("PlatFormCommand").to(PlatFormCommand).inSingletonScope()
bind("UpdateCommand").to(UpdateCommand).inSingletonScope() bind("UpdateCommand").to(UpdateCommand).inSingletonScope()
}) })
async function destroyAllCommand(ioc: Container) { async function destroyAllCommand(ioc: Container) {
await ioc.unloadAsync(modules) await ioc.unloadAsync(modules)
} }
export { modules, destroyAllCommand } 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" import { ApiFactory } from "common/lib/abstract"
class PlatForm extends _Base { class PlatForm extends _Base {
constructor() { constructor() {
super() super()
} }
private get api() { private get api() {
return ApiFactory.getApiClient() return ApiFactory.getApiClient()
} }
async showAbout() { async showAbout() {
return this.api.call("BasicService.showAbout") return this.api.call("BasicService.showAbout")
} }
async isFullScreen() { async isFullScreen() {
return this.api.call("PlatFormCommand.isFullscreen") return this.api.call("PlatFormCommand.isFullscreen")
} }
async toggleFullScreen() { async toggleFullScreen() {
return this.api.call("PlatFormCommand.fullscreen") return this.api.call("PlatFormCommand.fullscreen")
} }
async toggleDevTools() { async toggleDevTools() {
return this.api.call("PlatFormCommand.toggleDevTools") return this.api.call("PlatFormCommand.toggleDevTools")
} }
} }
export { PlatForm } 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" import WindowManager from "main/modules/window-manager"
export default class PlatFormCommand { export default class PlatFormCommand {
constructor( constructor(
@inject(WindowManager) private _WindowManager: WindowManager, @inject(WindowManager) private _WindowManager: WindowManager,
@inject(Tabs) private _Tabs: Tabs, @inject(Tabs) private _Tabs: Tabs,
) {} ) {}
setTheme(theme: typeof nativeTheme.themeSource) { setTheme(theme: typeof nativeTheme.themeSource) {
nativeTheme.themeSource = theme nativeTheme.themeSource = theme
} }
setTitlBar(options: TitleBarOverlayOptions) { setTitlBar(options: TitleBarOverlayOptions) {
const mainWindow = this._WindowManager.getMainWindow() const mainWindow = this._WindowManager.getMainWindow()
if (mainWindow) { if (mainWindow) {
mainWindow.setTitleBarOverlay(options) mainWindow.setTitleBarOverlay(options)
}
} }
}
showAbout() { showAbout() {
this._WindowManager.showWindow("about") this._WindowManager.showWindow("about")
} }
toggleDevTools() { toggleDevTools() {
const focusedWindow = this._WindowManager.getFocusWindow() const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) { if (focusedWindow) {
// @ts-ignore ... // @ts-ignore ...
focusedWindow.toggleDevTools() focusedWindow.toggleDevTools()
}
} }
fullscreen() { }
const focusedWindow = this._WindowManager.getFocusWindow() fullscreen() {
if (focusedWindow) { const focusedWindow = this._WindowManager.getFocusWindow()
const isFullScreen = focusedWindow!.isFullScreen() if (focusedWindow) {
focusedWindow!.setFullScreen(!isFullScreen) const isFullScreen = focusedWindow!.isFullScreen()
} focusedWindow!.setFullScreen(!isFullScreen)
} }
}
isFullscreen() { isFullscreen() {
const focusedWindow = this._WindowManager.getFocusWindow() const focusedWindow = this._WindowManager.getFocusWindow()
if (focusedWindow) { if (focusedWindow) {
return focusedWindow!.isFullScreen() return focusedWindow!.isFullScreen()
}
return false
} }
return false
}
relunch() { relunch() {
app.relaunch() app.relaunch()
app.exit() app.exit()
} }
reload() { reload() {
const focusedWindow = this._WindowManager.getFocusWindow() const focusedWindow = this._WindowManager.getFocusWindow()
// 重载之后, 刷新并关闭所有的次要窗体 // 重载之后, 刷新并关闭所有的次要窗体
if (this._WindowManager.length() > 1 && focusedWindow && focusedWindow.$$opts!.name === this._WindowManager.mainInfo.name) { if (this._WindowManager.length() > 1 && focusedWindow && focusedWindow.$$opts!.name === this._WindowManager.mainInfo.name) {
const choice = dialog.showMessageBoxSync(focusedWindow, { const choice = dialog.showMessageBoxSync(focusedWindow, {
type: "question", type: "question",
buttons: ["取消", "是的,继续", "不,算了"], buttons: ["取消", "是的,继续", "不,算了"],
title: "警告", title: "警告",
defaultId: 2, defaultId: 2,
cancelId: 0, cancelId: 0,
message: "警告", message: "警告",
detail: "重载主窗口将关闭所有子窗口,是否继续", detail: "重载主窗口将关闭所有子窗口,是否继续",
}) })
if (choice == 1) { if (choice == 1) {
this._WindowManager.getWndows().forEach(win => { this._WindowManager.getWndows().forEach(win => {
if (win.$$opts!.name !== this._WindowManager.mainInfo.name) { if (win.$$opts!.name !== this._WindowManager.mainInfo.name) {
win.close() win.close()
} }
}) })
} else { } else {
return return
} }
}
this._Tabs.closeAll()
focusedWindow!.reload()
} }
this._Tabs.closeAll()
focusedWindow!.reload()
}
} }

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

@ -1,54 +1,54 @@
import { _Base } from "../../lib/_Base" import { _Base } from "../../lib/_Base"
export class Tabs extends _Base { export class Tabs extends _Base {
constructor() { constructor() {
super() super()
} }
private isListen: boolean = false private isListen: boolean = false
private execUpdate = (...args) => { private execUpdate = (...args) => {
this.#fnList.forEach(v => v(...args)) this.#fnList.forEach(v => v(...args))
} }
#fnList: ((...args) => void)[] = [] #fnList: ((...args) => void)[] = []
listenUpdate(cb: (...args) => void) { listenUpdate(cb: (...args) => void) {
if (!this.isListen) { if (!this.isListen) {
api.on("main:TabsCommand.update", this.execUpdate) api.on("main:TabsCommand.update", this.execUpdate)
this.isListen = true this.isListen = true
} }
this.#fnList.push(cb) this.#fnList.push(cb)
} }
unListenUpdate(fn: (...args) => void) { unListenUpdate(fn: (...args) => void) {
this.#fnList = this.#fnList.filter(v => { this.#fnList = this.#fnList.filter(v => {
return v !== fn return v !== fn
}) })
if (!this.#fnList.length) { if (!this.#fnList.length) {
api.off("main:TabsCommand.update", this.execUpdate) api.off("main:TabsCommand.update", this.execUpdate)
this.isListen = false this.isListen = false
} }
} }
bindPosition(data) { bindPosition(data) {
api.call("TabsCommand.bindElement", data) api.call("TabsCommand.bindElement", data)
} }
closeAll() { closeAll() {
api.call("TabsCommand.closeAll") api.call("TabsCommand.closeAll")
} }
sync() { sync() {
api.call("TabsCommand.sync") api.call("TabsCommand.sync")
} }
unListenerAll() { unListenerAll() {
this.#fnList = [] this.#fnList = []
api.offAll("main:TabsCommand.update") api.offAll("main:TabsCommand.update")
} }
async getAllTabs() { async getAllTabs() {
const res = await api.call("TabsCommand.getAllTabs") const res = await api.call("TabsCommand.getAllTabs")
return res 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" import { broadcast } from "main/utils"
class TabsCommand { class TabsCommand {
constructor( constructor(
@inject(Tabs) private _Tabs: Tabs, @inject(Tabs) private _Tabs: Tabs,
@inject(WindowManager) private _WindowManager: WindowManager, @inject(WindowManager) private _WindowManager: WindowManager,
) { ) {
this._Tabs.events.on("update", this.listenerTabActive) this._Tabs.events.on("update", this.listenerTabActive)
} }
listenerTabActive = () => { listenerTabActive = () => {
broadcast("main:TabsCommand.update", this.getAllTabs()) broadcast("main:TabsCommand.update", this.getAllTabs())
} }
bindElement(rect) { bindElement(rect) {
this._Tabs.updateRect(rect) this._Tabs.updateRect(rect)
} }
reload() { reload() {
this._WindowManager.getMainWindow()?.reload() this._WindowManager.getMainWindow()?.reload()
} }
sync() { sync() {
this.listenerTabActive() this.listenerTabActive()
if (!this.getAllTabs().length) { if (!this.getAllTabs().length) {
this.add("about:blank") this.add("about:blank")
}
} }
}
add(url) { add(url) {
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!) this._Tabs.add(url, true, this._WindowManager.getMainWindow()!)
} }
nagivate(index: number, url: string) { nagivate(index: number, url: string) {
this._Tabs.navigate(+index, url) this._Tabs.navigate(+index, url)
} }
closeAll() { closeAll() {
this._Tabs.closeAll() this._Tabs.closeAll()
} }
setActive(index) { setActive(index) {
this._Tabs.changeActive(index) this._Tabs.changeActive(index)
} }
closeTab(e) { closeTab(e) {
this._Tabs.remove(e.body.active) this._Tabs.remove(e.body.active)
} }
getAllTabs() { getAllTabs() {
return this._Tabs._tabs.map(v => ({ return this._Tabs._tabs.map(v => ({
url: v.url, url: v.url,
showUrl: v.showUrl, showUrl: v.showUrl,
title: v.title, title: v.title,
favicons: v.favicons, favicons: v.favicons,
isActive: v.isActive, isActive: v.isActive,
})) }))
} }
} }
export { TabsCommand } export { TabsCommand }

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

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

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

@ -2,9 +2,9 @@ import { inject } from "inversify"
import Updater from "main/modules/updater" import Updater from "main/modules/updater"
export default class PlatFormCommand { export default class PlatFormCommand {
constructor(@inject(Updater) private _Updater: Updater) {} constructor(@inject(Updater) private _Updater: Updater) {}
async triggerHotUpdate() { async triggerHotUpdate() {
await this._Updater.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" import { AllKeys } from "common/event/common"
function emitHotUpdateReady(...argu) { function emitHotUpdateReady(...argu) {
broadcast<AllKeys>("hot-update-ready", ...argu) broadcast<AllKeys>("hot-update-ready", ...argu)
} }
export { emitHotUpdateReady } export { emitHotUpdateReady }

16
src/common/lib/_Base.ts

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

78
src/common/lib/abstract.ts

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

44
src/common/lib/browser.ts

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

26
src/common/lib/electron.ts

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

8
src/common/usePlatform.ts

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

278
src/main/App copy.ts

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

120
src/main/App.ts

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

6
src/main/_ioc.ts

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

24
src/main/_iocClass.ts

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

6
src/main/base/base.ts

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

30
src/main/controller/BasicService.ts

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

54
src/main/controller/TabsService.ts

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

6
src/main/controller/_ioc.ts

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

90
src/main/index.ts

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

38
src/main/modules/_ioc.ts

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

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

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

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

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

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

@ -69,4 +69,3 @@
// }; // };
// ses.protocol.interceptBufferProtocol("https", interceptHandler); // 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" import WindowManager from "../window-manager"
export default class Commands extends BaseClass { export default class Commands extends BaseClass {
destroy() { destroy() {
// TODO // TODO
} }
constructor( constructor(
@inject(IOC) private _IOC: IOC, @inject(IOC) private _IOC: IOC,
@inject(WindowManager) private _WindowManager: WindowManager, @inject(WindowManager) private _WindowManager: WindowManager,
) { ) {
super() super()
} }
private async handleCommand(command: string, ...argus) { private async handleCommand(command: string, ...argus) {
const splitClass = command.split(".") const splitClass = command.split(".")
const run = await this._IOC.getAsync<any>(splitClass[0]) const run = await this._IOC.getAsync<any>(splitClass[0])
if (run) { if (run) {
const result: Promise<any> | any = run[splitClass[1]](...argus) const result: Promise<any> | any = run[splitClass[1]](...argus)
return [true, result] return [true, result]
}
return [false]
} }
return [false]
}
public async invoke(command, ...argus) { public async invoke(command, ...argus) {
const result = await this.handleCommand(command, ...argus) const result = await this.handleCommand(command, ...argus)
return result return result
} }
init() { init() {
ipcMain.addListener("command", async (event, key, command: string, ...argus) => { ipcMain.addListener("command", async (event, key, command: string, ...argus) => {
// console.log(event.sender); // console.log(event.sender);
try { try {
const [isExist, result] = await this.handleCommand(command, ...argus) const [isExist, result] = await this.handleCommand(command, ...argus)
if (isExist) { if (isExist) {
if (isPromise(result)) { if (isPromise(result)) {
result result
.then((res: any) => { .then((res: any) => {
event.reply(key, null, res ?? null) event.reply(key, null, res ?? null)
event.returnValue = res ?? null event.returnValue = res ?? null
}) })
.catch((err: Error) => { .catch((err: Error) => {
event.reply(key, err) event.reply(key, err)
event.returnValue = null
})
} else {
event.reply(key, null, result ?? null)
event.returnValue = result ?? null
}
} else {
event.reply(key, new Error(`不存在该命令:${command}`))
event.returnValue = null
}
} catch (error) {
event.reply(key, error)
event.returnValue = null event.returnValue = null
} })
}) } else {
event.reply(key, null, result ?? null)
ipcMain.on("x_popup_menu", (_, name: string, options: IPopupMenuOption) => { event.returnValue = result ?? null
const menu = new Menu() }
const readMenu = (items: IMenuItemOption[]) => { } else {
return items.map(opt => { event.reply(key, new Error(`不存在该命令:${command}`))
if (typeof opt._click_evt === "string") { event.returnValue = null
const evt: string = opt._click_evt }
opt.click = () => { } catch (error) {
// broadcast(evt) event.reply(key, error)
this.sendMessage(name, evt) event.returnValue = null
} }
} })
if (opt.submenu && Array.isArray(opt.submenu)) {
opt.submenu = readMenu(opt.submenu)
}
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) }
if (opt.submenu && Array.isArray(opt.submenu)) {
arrays.forEach(v => { opt.submenu = readMenu(opt.submenu)
const item = new MenuItem(v) }
menu.append(item)
})
menu.on("menu-will-close", () => { return opt
this.sendMessage(name, `popup_menu_close:${options.menu_id}`)
// broadcast(`popup_menu_close:${options.menu_id}`)
})
menu.popup(options.popupOptions)
}) })
} }
const arrays = readMenu(options.items)
sendMessage(name: string, evt: string, ...argu: any[]) { arrays.forEach(v => {
const win = this._WindowManager.get(name) const item = new MenuItem(v)
if (win) { menu.append(item)
win.webContents.send(evt, ...argu) })
}
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" import fs from "fs-extra"
export class CustomAdapter<T> extends JSONFile<T> { export class CustomAdapter<T> extends JSONFile<T> {
constructor(filepath: string) { constructor(filepath: string) {
super(filepath) super(filepath)
this.filepath = filepath this.filepath = filepath
}
filepath: string = ""
async read() {
if (!fs.existsSync(this.filepath)) {
return null
} }
filepath: string = "" const data = fs.readJSONSync(this.filepath, { throws: false })
async read() { if (!data) {
if (!fs.existsSync(this.filepath)) { return null
return null
}
const data = fs.readJSONSync(this.filepath, { throws: false })
if (!data) {
return null
}
return data
} }
return data
}
async write(data: T) { async write(data: T) {
fs.ensureFileSync(this.filepath) fs.ensureFileSync(this.filepath)
await super.write(data) await super.write(data)
} }
} }
export class CustomLow<T> extends Low<T> { export class CustomLow<T> extends Low<T> {
constructor(adapter: CustomAdapter<T>, defaultData: T) { constructor(adapter: CustomAdapter<T>, defaultData: T) {
super(adapter, defaultData) super(adapter, defaultData)
this.filepath = adapter.filepath this.filepath = adapter.filepath
} }
filepath: string = "" filepath: string = ""
} }

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

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

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

@ -1,6 +1,6 @@
export function Layout(width, height) { export function Layout(width, height) {
// Tab布局位置 // Tab布局位置
const NavbarHeight = 30 const NavbarHeight = 30
const OffsetHeight = NavbarHeight + 100 const OffsetHeight = NavbarHeight + 100
return { x: 0, y: OffsetHeight, width: width, height: height - OffsetHeight } 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") const debug = _debug("app:tab")
interface IOption { interface IOption {
url: string url: string
active: boolean active: boolean
} }
interface IRect { interface IRect {
x: number x: number
y: number y: number
width: number width: number
height: number height: number
} }
class Tab extends BaseClass { class Tab extends BaseClass {
init() { init() {
// TODO // TODO
} }
public url: string = "" public url: string = ""
public showUrl: string = "" public showUrl: string = ""
public title: string = "" public title: string = ""
public favicons: string[] = [] public favicons: string[] = []
public active: boolean = false public active: boolean = false
public alive: boolean = false public alive: boolean = false
public isDestory: boolean = false public isDestory: boolean = false
public playing: boolean = false public playing: boolean = false
public visible: boolean = false public visible: boolean = false
private webContentsView: WebContentsView | null = null private webContentsView: WebContentsView | null = null
private curWindow: BrowserWindow | null = null private curWindow: BrowserWindow | null = null
private curRect: private curRect:
| { | {
x: number x: number
y: number y: number
width: number width: number
height: number height: number
} }
| undefined = undefined | undefined = undefined
private defaultOptions: IOption = { private defaultOptions: IOption = {
url: "", url: "",
active: false, active: false,
} }
private options: IOption private options: IOption
get isActive() { get isActive() {
return this.active return this.active
} }
get events() { get events() {
return this._events return this._events
} }
constructor(options = {}, window: BrowserWindow, curRect?: IRect) { constructor(options = {}, window: BrowserWindow, curRect?: IRect) {
super() super()
this.listenResize = this.listenResize.bind(this) this.listenResize = this.listenResize.bind(this)
this.options = { this.options = {
...this.defaultOptions, ...this.defaultOptions,
...options, ...options,
}
this.url = this.getUrl(this.options.url)
this.showUrl = this.options.url
this.curWindow = window
this.curRect = curRect
this.setActive(this.options.active)
} }
destroyTimer: NodeJS.Timeout | null = null this.url = this.getUrl(this.options.url)
stopDestroyTimer() { this.showUrl = this.options.url
if (this.destroyTimer !== null) { this.curWindow = window
clearTimeout(this.destroyTimer) this.curRect = curRect
this.destroyTimer = null this.setActive(this.options.active)
} }
} destroyTimer: NodeJS.Timeout | null = null
startDestroyTimer() { stopDestroyTimer() {
this.stopDestroyTimer() if (this.destroyTimer !== null) {
if (this.visible) return clearTimeout(this.destroyTimer)
if (this.playing) return this.destroyTimer = null
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)
} }
}
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) { setActive(active: boolean) {
if (!active) { if (!active) {
if (!this.webContentsView) return if (!this.webContentsView) return
this.curWindow!.removeListener("resize", this.listenResize) this.curWindow!.removeListener("resize", this.listenResize)
this.webContentsView.setVisible(false) this.webContentsView.setVisible(false)
this.visible = false this.visible = false
this.startDestroyTimer() this.startDestroyTimer()
} else { } else {
this.stopDestroyTimer() this.stopDestroyTimer()
this.visible = true this.visible = true
if (!this.webContentsView) { if (!this.webContentsView) {
this.create() this.create()
// , this.curWindow!.contentView.children.length - 1 // , this.curWindow!.contentView.children.length - 1
this.curWindow!.contentView.addChildView(this.webContentsView!) this.curWindow!.contentView.addChildView(this.webContentsView!)
this.alive = true this.alive = true
this.events.emit("update") this.events.emit("update")
} }
this.listenResize() this.listenResize()
this.curWindow!.addListener("resize", this.listenResize) this.curWindow!.addListener("resize", this.listenResize)
this.webContentsView!.setVisible(true) this.webContentsView!.setVisible(true)
}
this.active = active
} }
this.active = active
}
openDevtool() { openDevtool() {
if (!this.webContentsView) return if (!this.webContentsView) return
this.webContentsView.webContents.openDevTools({ this.webContentsView.webContents.openDevTools({
mode: "right", mode: "right",
}) })
} }
reload() { reload() {
if (!this.webContentsView) return if (!this.webContentsView) return
this.webContentsView.webContents.reload() 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() { // const el = document.elementFromPoint(x,y);
let securityAttr: Partial<WebPreferences> = {} // console.log(el)
if (this.url.startsWith("file:")) { // el.dispatchEvent(ev);
// 预加载脚本 // }
securityAttr = { // console.log("点击初始化完成")
preload: join(__dirname, "../preload/index.mjs"), // `)
sandbox: false, this.webContentsView.webContents.setWindowOpenHandler(ev => {
} debug(ev)
} this.events.emit("window-open", ev)
this.webContentsView = new WebContentsView({ return { action: "deny" }
webPreferences: { })
sandbox: true, webContents.addListener("media-paused", () => {
webSecurity: true, this.playing = false
allowRunningInsecureContent: false, this.startDestroyTimer()
nodeIntegration: false, })
spellcheck: false, webContents.addListener("media-started-playing", () => {
contextIsolation: true, this.playing = true
...securityAttr, this.stopDestroyTimer()
}, })
}) webContents.addListener("did-finish-load", () => {
const webContents = this.webContentsView.webContents this.url = webContents.getURL()
this.webContentsView.webContents.loadURL(this.url) this.showUrl = this.getShowUrl(this.url)
// this.webContentsView.webContents.executeJavaScript(` this.events.emit("update")
// const click = (x,y)=>{ })
// const ev = new MouseEvent("click", { webContents.addListener("did-navigate-in-page", () => {
// view: window, this.url = webContents.getURL()
// bubbles: true, this.showUrl = this.getShowUrl(this.url)
// cancelable: true, this.events.emit("update")
// screenX: x, })
// screenY: y 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); print() {
// console.log(el) return {
// el.dispatchEvent(ev); url: this.url,
// } showUrl: this.showUrl,
// 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() { private getUrl(url) {
return { if (url === "about:blank") {
url: this.url, debug(FuckHTML)
showUrl: this.showUrl, return pathToFileURL(FuckHTML).href
}
} }
return url
}
private getUrl(url) { private getShowUrl(url) {
if (url === "about:blank") { try {
debug(FuckHTML) if (fileURLToPath(url) === FuckHTML) {
return pathToFileURL(FuckHTML).href debug(url)
} debug(FuckHTML)
return url return "about:blank"
}
} catch (error) {
// ignore
} }
return url
}
private getShowUrl(url) { listenResize() {
try { if (!this.curWindow) {
if (fileURLToPath(url) === FuckHTML) { return
debug(url)
debug(FuckHTML)
return "about:blank"
}
} catch (error) {
// ignore
}
return url
} }
if (!this.webContentsView) {
listenResize() { return
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.curRect) {
updateRect(curRect: IRect) { return
this.curRect = curRect
if (!this.webContentsView) {
return
}
this.webContentsView.setBounds(this.curRect)
} }
this.webContentsView.setBounds(this.curRect)
// const size = this.curWindow.getContentSize()
// this.webContentsView.setBounds(Layout(size[0], size[1]))
}
navigate(url: string) { updateRect(curRect: IRect) {
if (!this.webContentsView) return this.curRect = curRect
this.webContentsView.webContents.loadURL(this.getUrl(url)) if (!this.webContentsView) {
return
} }
this.webContentsView.setBounds(this.curRect)
}
#destoryWebContentsView() { navigate(url: string) {
this.stopDestroyTimer() if (!this.webContentsView) return
if (this.webContentsView && this.curWindow && !this.curWindow.isDestroyed()) { this.webContentsView.webContents.loadURL(this.getUrl(url))
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() { #destoryWebContentsView() {
this.#destoryWebContentsView() this.stopDestroyTimer()
this.events.removeAllListeners() if (this.webContentsView && this.curWindow && !this.curWindow.isDestroyed()) {
this.isDestory = true this.curWindow.contentView.removeChildView(this.webContentsView)
debug("Tab destroy") 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 } export { Tab }

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

@ -5,121 +5,121 @@ import { BrowserWindow } from "electron"
import EventEmitter from "events" import EventEmitter from "events"
interface IRect { interface IRect {
x: number x: number
y: number y: number
width: number width: number
height: number height: number
} }
const debug = _debug("app:tabs") const debug = _debug("app:tabs")
class Tabs extends BaseClass { class Tabs extends BaseClass {
destroy() { destroy() {
this._tabs.forEach(v => v.destroy()) this._tabs.forEach(v => v.destroy())
this._tabs = [] 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)
} }
this.events.emit("update")
public events = new EventEmitter() }
private curRect: changeActive(index: number) {
| { this._tabs.forEach((tab, i) => {
x: number tab.setActive(i === index)
y: number })
width: number this.events.emit("update", index)
height: number }
}
| undefined = undefined openDevtool(index: number) {
if (this._tabs[index]) {
constructor() { this._tabs[index].openDevtool()
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)
}
} }
}
remove(index: number) { reload(index: number) {
this._tabs[index].destroy() if (this._tabs[index]) {
if (this._tabs[index].isActive && index - 1 >= 0) { this._tabs[index].reload()
this.changeActive(index - 1)
}
this._tabs.splice(index, 1)
this.events.emit("update")
} }
}
closeAll() { navigate(index: number, url: string) {
this._tabs = this._tabs.filter(tab => { if (this._tabs[index]) {
tab.destroy() this._tabs[index].navigate(url)
})
this._tabs = []
this.events.emit("update")
} }
}
removeAll(index: number[]) { remove(index: number) {
index this._tabs[index].destroy()
.map(v => { if (this._tabs[index].isActive && index - 1 >= 0) {
return this._tabs[+v] this.changeActive(index - 1)
})
.forEach(tab => {
tab.destroy()
})
this._tabs = this._tabs.filter(v => {
return !v.isDestory
})
this.events.emit("update")
} }
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 } export { Tabs }

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

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

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

@ -13,121 +13,121 @@ const { autoUpdater } = pkg
@injectable() @injectable()
export class Updater extends BaseClass { export class Updater extends BaseClass {
public events = new EventEmitter() public events = new EventEmitter()
private timer: ReturnType<typeof setInterval> | null = null private timer: ReturnType<typeof setInterval> | null = null
// autoReplace = false // autoReplace = false
async triggerHotUpdate(autoReplace = false) { async triggerHotUpdate(autoReplace = false) {
await fetchHotUpdatePackage() await fetchHotUpdatePackage()
flagNeedUpdate() flagNeedUpdate()
if (!autoReplace) { if (!autoReplace) {
dialog.showMessageBox({ dialog.showMessageBox({
title: Locales.t("update.ready.hot.title"), title: Locales.t("update.ready.hot.title"),
message: Locales.t("update.ready.hot.desc", { version: app.getVersion() }), message: Locales.t("update.ready.hot.desc", { version: app.getVersion() }),
}) })
} else { } else {
app.quit() app.quit()
}
} }
}
constructor() {
super() constructor() {
super()
// 配置自动更新
autoUpdater.autoDownload = false // 配置自动更新
autoUpdater.autoInstallOnAppQuit = true autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
// 检查更新错误
autoUpdater.on("error", error => { // 检查更新错误
debug("Update error:", error) autoUpdater.on("error", error => {
}) debug("Update error:", error)
})
// 检查更新
autoUpdater.on("checking-for-update", () => { // 检查更新
debug("Checking for updates...") autoUpdater.on("checking-for-update", () => {
}) debug("Checking for updates...")
})
// 有可用更新
autoUpdater.on("update-available", info => { // 有可用更新
debug("Update available:", info) autoUpdater.on("update-available", info => {
this.promptUserToUpdate() debug("Update available:", info)
}) this.promptUserToUpdate()
})
// 没有可用更新
autoUpdater.on("update-not-available", info => { // 没有可用更新
debug("Update not available:", info) autoUpdater.on("update-not-available", info => {
}) debug("Update not available:", info)
})
// 更新下载进度
autoUpdater.on("download-progress", progressObj => { // 更新下载进度
debug( autoUpdater.on("download-progress", progressObj => {
`Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`, debug(
) `Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`,
}) )
})
// 更新下载完成
autoUpdater.on("update-downloaded", info => { // 更新下载完成
debug("Update downloaded:", info) autoUpdater.on("update-downloaded", info => {
this.promptUserToInstall() debug("Update downloaded:", info)
}) this.promptUserToInstall()
} })
}
init() {
// 定期检查更新 init() {
// 定期检查更新
this.checkForUpdates()
this.timer && clearInterval(this.timer)
this.timer = setInterval(
() => {
this.checkForUpdates() this.checkForUpdates()
this.timer && clearInterval(this.timer) },
this.timer = setInterval( 1000 * 60 * 60,
() => { ) // 每小时检查一次
this.checkForUpdates() }
},
1000 * 60 * 60, destroy() {
) // 每小时检查一次 // 清理工作
} if (this.timer) {
clearInterval(this.timer)
destroy() { this.timer = null
// 清理工作
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
} }
}
private async checkForUpdates() {
if (app.isPackaged) { private async checkForUpdates() {
try { if (app.isPackaged) {
await autoUpdater.checkForUpdates() try {
} catch (error) { await autoUpdater.checkForUpdates()
debug("Failed to check for updates:", error) } catch (error) {
} debug("Failed to check for updates:", error)
} }
} }
}
private async promptUserToUpdate() {
const result = await dialog.showMessageBox({ private async promptUserToUpdate() {
type: "info", const result = await dialog.showMessageBox({
title: "发现新版本", type: "info",
message: "是否下载新版本?", title: "发现新版本",
buttons: ["下载", "暂不更新"], message: "是否下载新版本?",
defaultId: 0, buttons: ["下载", "暂不更新"],
}) defaultId: 0,
})
if (result.response === 0) {
autoUpdater.downloadUpdate() if (result.response === 0) {
} autoUpdater.downloadUpdate()
} }
}
private async promptUserToInstall() {
const result = await dialog.showMessageBox({ private async promptUserToInstall() {
type: "info", const result = await dialog.showMessageBox({
title: "更新已就绪", type: "info",
message: "新版本已下载完成,是否立即安装?", title: "更新已就绪",
buttons: ["立即安装", "稍后安装"], message: "新版本已下载完成,是否立即安装?",
defaultId: 0, buttons: ["立即安装", "稍后安装"],
}) defaultId: 0,
})
if (result.response === 0) {
autoUpdater.quitAndInstall(false, true) if (result.response === 0) {
} autoUpdater.quitAndInstall(false, true)
} }
}
} }
export default Updater 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") const debug = _debug("app:window-manager")
declare module "electron" { declare module "electron" {
interface BrowserWindow { interface BrowserWindow {
$$forceClose?: boolean $$forceClose?: boolean
$$lastChoice?: number $$lastChoice?: number
$$opts?: Param $$opts?: Param
} }
} }
export { WindowManager } export { WindowManager }
export default class WindowManager extends BaseClass { export default class WindowManager extends BaseClass {
constructor() { constructor() {
super() super()
this.isMainShowReady = new Promise(resolve => { this.isMainShowReady = new Promise(resolve => {
this.isMainShowResolve = resolve this.isMainShowResolve = resolve
}) })
} }
destroy() { destroy() {
// TODO // TODO
}
globalChioce: number = -1
#showWin(info: Param) {
if (this.#windows.length >= 6) {
dialog.showErrorBox("错误", "窗口数量超出限制")
return
} }
globalChioce: number = -1 if (!info.name) {
#showWin(info: Param) { dialog.showErrorBox("错误", "窗口未指定唯一key")
if (this.#windows.length >= 6) { return
dialog.showErrorBox("错误", "窗口数量超出限制") }
return const index = this.findIndex(info.name)
} if (index === -1) {
if (!info.name) { this.#windows.push(this.#add(info))
dialog.showErrorBox("错误", "窗口未指定唯一key") } else {
return if (this.#windows[index].isDestroyed()) {
} this.#windows[index] = this.#add(info)
const index = this.findIndex(info.name) } else {
if (index === -1) { if (info.url && info.loadURLInSameWin) {
this.#windows.push(this.#add(info)) this.#windows[index].loadURL(info.url)
} 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()
}
} }
this.showCurrentWindow() this.#windows[index].show()
}
} }
this.showCurrentWindow()
}
showMainWindow() { showMainWindow() {
this.#showWin(this.mainInfo) this.#showWin(this.mainInfo)
this.isMainShowResolve() this.isMainShowResolve()
} }
private isMainShowResolve private isMainShowResolve
private isMainShowReady private isMainShowReady
async waitMainShowReady() { async waitMainShowReady() {
await this.isMainShowReady await this.isMainShowReady
} }
showWindow(name: string, opts?: Partial<IConfig>) { showWindow(name: string, opts?: Partial<IConfig>) {
let have = false let have = false
for (const key in this.#urlMap) { for (const key in this.#urlMap) {
const info = this.#urlMap[key] const info = this.#urlMap[key]
if (new RegExp(key).test(name)) { if (new RegExp(key).test(name)) {
opts && merge(info, opts) opts && merge(info, opts)
info.name = name info.name = name
if (!info.ignoreEmptyUrl && !info.url) { if (!info.ignoreEmptyUrl && !info.url) {
dialog.showErrorBox("错误", name + "窗口未提供url") dialog.showErrorBox("错误", name + "窗口未提供url")
return return
}
this.#showWin(info as Param)
have = true
}
}
if (!have) {
dialog.showErrorBox("错误", name + "窗口未创建成功")
return
} }
this.#showWin(info as Param)
have = true
}
} }
if (!have) {
init() { dialog.showErrorBox("错误", name + "窗口未创建成功")
/** return
*
*/
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()
}
})
} }
}
#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() { app.on("window-all-closed", () => {
return this.#windows if (process.platform !== "darwin") {
} app.quit()
}
})
}
length() { #urlMap = getWindowsMap()
return this.#windows.length
}
public get mainInfo() { getWndows() {
return this.#urlMap["main"] as Param 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) { #windows: BrowserWindow[] = []
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
// }
// // 禁用 Node.js 集成 #defaultConfig: IConfig = defaultConfig
// webPreferences.nodeIntegration = false
// // 验证正在加载的 URL #add(config: Param) {
// // if (!params.src.startsWith('https://example.com/')) { const curConfig = cloneDeep(this.#defaultConfig ?? {})
// // event.preventDefault() for (const key in config) {
// // } if (Object.prototype.hasOwnProperty.call(config, key)) {
// }) const value = config[key]
if (curConfig.type === "info") { // if (Reflect.has(curConfig, key)) {
// 隐藏菜单 curConfig[key] = value
browserWin.setMenuBarVisibility(false) // }
} }
if (curConfig.url) { }
browserWin.loadURL(curConfig.url) const privateConfig = merge(curConfig.overideWindowOpts ? {} : cloneDeep(defaultWindowConfig), curConfig.windowOpts ?? {})
// logger.debug(`当前窗口网址:${curConfig.url}`) 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) { if (browserWin.$$forceClose) {
browserWin.once("ready-to-show", () => { that.delete(curConfig.name)
debug(`准备展示:`, curConfig.url) app.quit()
browserWin?.show() } else {
}) let choice = -1
} else { if (browserWin && browserWin!.$$lastChoice !== undefined && browserWin.$$lastChoice >= 0) {
browserWin?.show() 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 if (choice === 1) {
} justQuit()
showCurrentWindow() {
if (this.#windows.length) {
debug(`current open window: ${this.#windows.map(v => v.$$opts!.name).join(",")}`)
} else { } 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) { // // 禁用 Node.js 集成
for (let i = this.#windows.length - 1; i >= 0; i--) { // webPreferences.nodeIntegration = false
const win = this.#windows[i]
if (name === win.$$opts!.name) {
win.destroy()
this.#windows.splice(i, 1)
}
}
this.showCurrentWindow()
}
get(name: string) { // // 验证正在加载的 URL
return this.#windows.find(v => { // // if (!params.src.startsWith('https://example.com/')) {
return v.$$opts!.name === name // // 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() { showCurrentWindow() {
const mainWindow = this.getMainWindow() if (this.#windows.length) {
if (mainWindow?.isFocused()) { debug(`current open window: ${this.#windows.map(v => v.$$opts!.name).join(",")}`)
return mainWindow } else {
} debug(`all closed`)
for (let i = 0; i < this.#windows.length; i++) {
const win = this.#windows[i]
if (win.isFocused()) {
return win
}
}
return
} }
}
getMainWindow() { #onClose(name: string) {
return this.#windows.find(v => { for (let i = this.#windows.length - 1; i >= 0; i--) {
return v.$$opts!.name === this.mainInfo.name const win = this.#windows[i]
}) if (name === win.$$opts!.name) {
win.destroy()
this.#windows.splice(i, 1)
}
} }
this.showCurrentWindow()
}
close(name: string | RegExp) { get(name: string) {
const indexList = this.findAllIndex(name) return this.#windows.find(v => {
for (let i = indexList.length - 1; i >= 0; i--) { return v.$$opts!.name === name
const index = indexList[i] })
const win = this.#windows[index] }
win.close()
} 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) { getMainWindow() {
const indexList = this.findAllIndex(name) return this.#windows.find(v => {
for (let i = indexList.length - 1; i >= 0; i--) { return v.$$opts!.name === this.mainInfo.name
const index = indexList[i] })
this.#windows.splice(index, 1) }
}
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) { delete(name: string | RegExp) {
const index = this.#windows.findIndex(v => { const indexList = this.findAllIndex(name)
if (typeof name === "string") { for (let i = indexList.length - 1; i >= 0; i--) {
return v.$$opts!.name === name const index = indexList[i]
} else { this.#windows.splice(index, 1)
return name.test(v.$$opts!.name)
}
})
return index
} }
}
findAllIndex(name: string | RegExp) { findIndex(name: string | RegExp) {
const result: number[] = [] const index = this.#windows.findIndex(v => {
for (let i = 0; i < this.#windows.length; i++) { if (typeof name === "string") {
const win = this.#windows[i] return v.$$opts!.name === name
if (typeof name === "string" && win.$$opts!.name === name) { } else {
result.push(i) return name.test(v.$$opts!.name)
} else if (typeof name !== "string" && name.test(win.$$opts!.name)) { }
result.push(i) })
} return index
} }
return result
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) { // show(name: string | RegExp) {
// let indexList = this.findAllIndex(name) // let indexList = this.findAllIndex(name)
// if (!!indexList.length) { // if (!!indexList.length) {
// for (let i = 0; i < indexList.length; i++) { // for (let i = 0; i < indexList.length; i++) {
// const index = indexList[i]; // const index = indexList[i];
// const win = this.#windows[index] // const win = this.#windows[index]
// if (win.isDestroyed()) { // if (win.isDestroyed()) {
// this.#windows[index] = this.#add(win.$$opts) // this.#windows[index] = this.#add(win.$$opts)
// } else { // } else {
// win.show() // win.show()
// } // }
// } // }
// } else { // } else {
// console.warn("该窗口不存在") // 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 type Param = Partial<IConfig> & Required<Pick<IConfig, "name">>
export interface IConfig { export interface IConfig {
name?: string name?: string
url?: string url?: string
loadURLInSameWin?: boolean loadURLInSameWin?: boolean
type?: "info" type?: "info"
windowOpts?: BrowserWindowConstructorOptions windowOpts?: BrowserWindowConstructorOptions
overideWindowOpts?: boolean overideWindowOpts?: boolean
ignoreEmptyUrl?: boolean ignoreEmptyUrl?: boolean
denyWindowOpen?: boolean denyWindowOpen?: boolean
confrimWindowClose?: boolean confrimWindowClose?: boolean
confrimWindowCloseText?: { confrimWindowCloseText?: {
title: string title: string
message: string message: string
buttons: string[] buttons: string[]
defaultId: number defaultId: number
cancelId: number cancelId: number
} }
} }
export const defaultConfig: IConfig = { export const defaultConfig: IConfig = {
denyWindowOpen: true, denyWindowOpen: true,
} }
export const defaultWindowConfig = { export const defaultWindowConfig = {
height: 600, height: 600,
useContentSize: true, useContentSize: true,
width: 800, width: 800,
show: true, show: true,
resizable: true, resizable: true,
minWidth: 900, minWidth: 900,
minHeight: 600, minHeight: 600,
frame: true, frame: true,
transparent: false, transparent: false,
alwaysOnTop: false, alwaysOnTop: false,
webPreferences: {}, webPreferences: {},
} }
export function getWindowsMap(): Record<string, IConfig> { export function getWindowsMap(): Record<string, IConfig> {
return { return {
main: { main: {
name: "main", name: "main",
url: getFileUrl("index.html"), url: getFileUrl("index.html"),
confrimWindowClose: true, confrimWindowClose: true,
confrimWindowCloseText: { confrimWindowCloseText: {
title: config.app_title, title: config.app_title,
defaultId: 0, defaultId: 0,
cancelId: 0, cancelId: 0,
message: "确定要关闭吗?", message: "确定要关闭吗?",
buttons: ["没事", "直接退出"], buttons: ["没事", "直接退出"],
}, },
windowOpts: { windowOpts: {
show: false, show: false,
titleBarStyle: "hidden", titleBarStyle: "hidden",
titleBarOverlay: true, titleBarOverlay: true,
icon: icon, icon: icon,
...(process.platform === "linux" ? { icon } : {}), ...(process.platform === "linux" ? { icon } : {}),
webPreferences: { webPreferences: {
webviewTag: false, webviewTag: false,
preload: join(__dirname, "../preload/index.mjs"), preload: join(__dirname, "../preload/index.mjs"),
nodeIntegration: true, nodeIntegration: true,
contextIsolation: true, contextIsolation: true,
},
},
}, },
_blank: { },
overideWindowOpts: false, },
confrimWindowClose: true, _blank: {
confrimWindowCloseText: { overideWindowOpts: false,
title: config.app_title, confrimWindowClose: true,
defaultId: 0, confrimWindowCloseText: {
cancelId: 0, title: config.app_title,
message: "确定要关闭吗?", defaultId: 0,
buttons: ["没事", "直接退出"], cancelId: 0,
}, message: "确定要关闭吗?",
type: "info", buttons: ["没事", "直接退出"],
windowOpts: { },
height: 600, type: "info",
useContentSize: true, windowOpts: {
width: 800, height: 600,
show: true, useContentSize: true,
resizable: true, width: 800,
minWidth: 900, show: true,
minHeight: 600, resizable: true,
frame: true, minWidth: 900,
transparent: false, minHeight: 600,
alwaysOnTop: false, frame: true,
icon: icon, transparent: false,
title: config.app_title, alwaysOnTop: false,
webPreferences: { icon: icon,
devTools: false, title: config.app_title,
sandbox: true, webPreferences: {
nodeIntegration: false, devTools: false,
contextIsolation: true, sandbox: true,
webviewTag: false, nodeIntegration: false,
preload: undefined, contextIsolation: true,
}, webviewTag: false,
}, preload: undefined,
}, },
"^about": { },
url: getFileUrl("about.html"), },
overideWindowOpts: true, "^about": {
confrimWindowClose: false, url: getFileUrl("about.html"),
type: "info", overideWindowOpts: true,
windowOpts: { confrimWindowClose: false,
width: 600, type: "info",
height: 200, windowOpts: {
minimizable: false, width: 600,
darkTheme: true, height: 200,
modal: true, minimizable: false,
show: false, darkTheme: true,
resizable: false, modal: true,
icon: icon, show: false,
webPreferences: { resizable: false,
devTools: false, icon: icon,
sandbox: false, webPreferences: {
nodeIntegration: false, devTools: false,
contextIsolation: true, 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() @injectable()
class Zephyr extends BaseClass { class Zephyr extends BaseClass {
// private readonly ALLOWED_PATHS: string[] = [] // 可以在这里定义允许访问的路径白名单 // private readonly ALLOWED_PATHS: string[] = [] // 可以在这里定义允许访问的路径白名单
private readonly ALLOWED_EXTENSIONS: string[] = [".txt", ".json", ".md"] // 允许的文件类型 private readonly ALLOWED_EXTENSIONS: string[] = [".txt", ".json", ".md"] // 允许的文件类型
private readonly MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB private readonly MAX_FILE_SIZE = 50 * 1024 * 1024 // 50MB
// private readonly SAFE_PATH_PATTERN = /^[a-zA-Z0-9\s\-_\/\\:\.]+$/ // private readonly SAFE_PATH_PATTERN = /^[a-zA-Z0-9\s\-_\/\\:\.]+$/
// 定义操作类型 // 定义操作类型
private readonly OPERATIONS = { private readonly OPERATIONS = {
READ: "r", READ: "r",
WRITE: "w", WRITE: "w",
READWRITE: "rw", READWRITE: "rw",
TEMP: "t", TEMP: "t",
} as const } as const
private readonly pathConfig: { private readonly pathConfig: {
read: string[] read: string[]
temp: string[] temp: string[]
write: string[] write: string[]
} = { } = {
read: [], read: [],
temp: [], temp: [],
write: [], write: [],
}
// 文件锁定相关
private readonly fileLocks = new Map<string, boolean>()
// 访问频率限制相关
private readonly rateLimiter = new Map<string, number>()
private readonly MAX_REQUESTS = 10 // 每个文件在时间窗口内的最大请求次数
private readonly WINDOW_MS = 60000 // 时间窗口:1分钟
// 审计日志相关
private readonly LOG_FILE = path.join(app.getPath("logs"), "zephyr-access.log")
constructor() {
super()
this.interceptHandlerZephyr = this.interceptHandlerZephyr.bind(this)
this.initLogFile()
debug("zephyr init")
}
private async initLogFile() {
const logDir = path.dirname(this.LOG_FILE)
await fs.promises.mkdir(logDir, { recursive: true })
}
// 文件锁定机制
private async acquireFileLock(filePath: string): Promise<boolean> {
if (this.fileLocks.get(filePath)) {
return false
} }
this.fileLocks.set(filePath, true)
// 文件锁定相关 return true
private readonly fileLocks = new Map<string, boolean>() }
// 访问频率限制相关 private releaseFileLock(filePath: string): void {
private readonly rateLimiter = new Map<string, number>() this.fileLocks.delete(filePath)
private readonly MAX_REQUESTS = 10 // 每个文件在时间窗口内的最大请求次数 }
private readonly WINDOW_MS = 60000 // 时间窗口:1分钟
// 访问频率限制
// 审计日志相关 private isRateLimited(filePath: string): boolean {
private readonly LOG_FILE = path.join(app.getPath("logs"), "zephyr-access.log") // const now = Date.now()
const count = this.rateLimiter.get(filePath) || 0
constructor() {
super() if (count >= this.MAX_REQUESTS) {
this.interceptHandlerZephyr = this.interceptHandlerZephyr.bind(this) debug("访问频率超限:", filePath)
this.initLogFile() return true
debug("zephyr init")
} }
private async initLogFile() { this.rateLimiter.set(filePath, count + 1)
const logDir = path.dirname(this.LOG_FILE) setTimeout(() => {
await fs.promises.mkdir(logDir, { recursive: true }) const currentCount = this.rateLimiter.get(filePath)
if (currentCount && currentCount > 0) {
this.rateLimiter.set(filePath, currentCount - 1)
}
}, this.WINDOW_MS)
return false
}
// 审计日志
private async logAccess(operation: string, filePath: string, success: boolean, details?: string) {
const timestamp = new Date().toISOString()
const logEntry = {
timestamp,
operation,
filePath,
success,
details,
} }
// 文件锁定机制 try {
private async acquireFileLock(filePath: string): Promise<boolean> { await fs.promises.appendFile(this.LOG_FILE, JSON.stringify(logEntry) + "\n", "utf8")
if (this.fileLocks.get(filePath)) { } catch (error) {
return false debug("写入审计日志失败:", error)
}
this.fileLocks.set(filePath, true)
return true
} }
}
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 async isPathSafe(filePath: string, operation: string): Promise<boolean> {
private isRateLimited(filePath: string): boolean { try {
// const now = Date.now() // 1. 基本路径检查
const count = this.rateLimiter.get(filePath) || 0 if (!this.isValidPath(filePath)) {
debug("不安全的路径字符:", filePath)
return false
}
if (count >= this.MAX_REQUESTS) { // 2. 检查是否包含 .. 路径
debug("访问频率超限:", filePath) if (filePath.includes("..")) {
return true debug("检测到路径遍历尝试")
} return false
}
this.rateLimiter.set(filePath, count + 1) // 3. 检查符号链接
setTimeout(() => { if (await this.isSymlink(filePath)) {
const currentCount = this.rateLimiter.get(filePath) debug("不允许访问符号链接")
if (currentCount && currentCount > 0) { return false
this.rateLimiter.set(filePath, currentCount - 1) }
}
}, this.WINDOW_MS)
// 4. 检查文件大小
if (!(await this.checkFileSize(filePath))) {
debug("文件超出大小限制")
return false return false
} }
// 审计日志 // 5. 文件类型检查
private async logAccess(operation: string, filePath: string, success: boolean, details?: string) { const ext = path.extname(filePath).toLowerCase()
const timestamp = new Date().toISOString() if (!this.ALLOWED_EXTENSIONS.includes(ext)) {
const logEntry = { debug("不允许的文件类型:", ext)
timestamp, return false
operation, }
filePath,
success,
details,
}
try { // 6. 权限检查
await fs.promises.appendFile(this.LOG_FILE, JSON.stringify(logEntry) + "\n", "utf8") const allowedPaths = this.getPathsByOperation(operation)
} catch (error) { if (!allowedPaths) return false
debug("写入审计日志失败:", error)
}
}
// 文件内容验证 // 7. 确保路径在允许范围内
private async validateFileContent(filePath: string): Promise<boolean> { const isInAllowedPath = allowedPaths.some(allowedPath => {
try { const resolvedAllowed = path.resolve(allowedPath)
const ext = path.extname(filePath).toLowerCase() const resolvedTarget = path.resolve(filePath)
const content = await fs.promises.readFile(filePath, "utf8") return resolvedTarget.startsWith(resolvedAllowed)
})
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() { if (!isInAllowedPath) {
const ses = session.defaultSession debug("路径不在允许范围内")
ses.protocol.unhandle("zephyr") return false
this.fileLocks.clear() }
this.rateLimiter.clear()
debug("zephyr destroyed")
}
init(partition?: string) { // 添加频率限制检查
const ses = partition ? session.fromPartition(partition) : session.defaultSession if (this.isRateLimited(filePath)) {
ses.protocol.handle("zephyr", this.interceptHandlerZephyr) await this.logAccess(operation, filePath, false, "访问频率超限")
debug("zephyr initialized with partition:", partition) return false
} }
setAllowedPaths(config: Partial<typeof this.pathConfig>) { // 添加文件内容验证
Object.assign(this.pathConfig, config) if (!(await this.validateFileContent(filePath))) {
debug("Updated allowed paths:", this.pathConfig) 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 { async interceptHandlerZephyr(request: Request): Promise<Response> {
// 规范化路径 try {
const normalizedPath = path.normalize(filePath) if (!request.url.startsWith("zephyr://")) {
return net.fetch(request.url, request)
// Windows 路径特殊处理 }
const isWindowsPath = /^[a-z]:/i.test(normalizedPath)
const urlParts = request.url.replace(/^zephyr:\/\//, "").split("/")
// 检查基本字符(排除特殊字符) const operation = urlParts[0]
// 允许驱动器冒号,但排除其他特殊字符 const filePath = path.normalize(urlParts.slice(1).join("/"))
const basicCheck = isWindowsPath ? /^[a-z]:[^<>"|?*]+$/i.test(normalizedPath) : /^[^<>:"|?*]+$/i.test(normalizedPath)
if (!operation || !filePath) {
return basicCheck && (isWindowsPath || normalizedPath.startsWith("/")) return new Response("Invalid URL format", { status: 400 })
} catch { }
return false
} 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 { private async handleReadOperation(filePath: string): Promise<Response> {
// 1. 基本路径检查 const cleanup = async (error?: Error): Promise<Response> => {
if (!this.isValidPath(filePath)) { this.releaseFileLock(filePath)
debug("不安全的路径字符:", filePath) if (error) {
return false await this.logAccess("READ", filePath, false, error.message)
} return new Response("Internal Server Error", { status: 500 })
}
// 2. 检查是否包含 .. 路径 return new Response("OK", { status: 200 })
if (filePath.includes("..")) {
debug("检测到路径遍历尝试")
return false
}
// 3. 检查符号链接
if (await this.isSymlink(filePath)) {
debug("不允许访问符号链接")
return false
}
// 4. 检查文件大小
if (!(await this.checkFileSize(filePath))) {
debug("文件超出大小限制")
return false
}
// 5. 文件类型检查
const ext = path.extname(filePath).toLowerCase()
if (!this.ALLOWED_EXTENSIONS.includes(ext)) {
debug("不允许的文件类型:", ext)
return false
}
// 6. 权限检查
const allowedPaths = this.getPathsByOperation(operation)
if (!allowedPaths) return false
// 7. 确保路径在允许范围内
const isInAllowedPath = allowedPaths.some(allowedPath => {
const resolvedAllowed = path.resolve(allowedPath)
const resolvedTarget = path.resolve(filePath)
return resolvedTarget.startsWith(resolvedAllowed)
})
if (!isInAllowedPath) {
debug("路径不在允许范围内")
return false
}
// 添加频率限制检查
if (this.isRateLimited(filePath)) {
await this.logAccess(operation, filePath, false, "访问频率超限")
return false
}
// 添加文件内容验证
if (!(await this.validateFileContent(filePath))) {
await this.logAccess(operation, filePath, false, "文件内容验证失败")
return false
}
await this.logAccess(operation, filePath, true)
return true
} catch (error: any) {
await this.logAccess(operation, filePath, false, error.message)
debug("路径安全检查错误:", error)
return false
}
} }
async interceptHandlerZephyr(request: Request): Promise<Response> { try {
try { if (!(await this.acquireFileLock(filePath))) {
if (!request.url.startsWith("zephyr://")) { await this.logAccess("READ", filePath, false, "文件已锁定")
return net.fetch(request.url, request) return new Response("File is locked", { status: 423 })
} }
const urlParts = request.url.replace(/^zephyr:\/\//, "").split("/") const stream = fs.createReadStream(filePath, {
const operation = urlParts[0] flags: "r",
const filePath = path.normalize(urlParts.slice(1).join("/")) encoding: "utf8",
})
if (!operation || !filePath) {
return new Response("Invalid URL format", { status: 400 }) const timeout = setTimeout(() => {
} stream.destroy()
cleanup(new Error("读取超时"))
if (!(await this.isPathSafe(filePath, operation))) { }, 5000)
debug("访问被拒绝:", filePath)
return new Response("Access Denied", { status: 403 }) const response = new Response(stream as any, {
} status: 200,
headers: {
// 处理不同的操作类型 "content-type": this.getContentType(filePath),
switch (operation) { "cache-control": "no-cache",
case this.OPERATIONS.READ: },
return await this.handleReadOperation(filePath) })
case this.OPERATIONS.WRITE:
return await this.handleWriteOperation(filePath, request) stream.on("error", error => {
default: clearTimeout(timeout)
return new Response("Operation not supported", { status: 400 }) cleanup(error)
} })
} catch (error) {
debug("处理请求错误:", error) stream.on("end", () => {
return new Response("Internal Server Error", { status: 500 }) 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> { try {
const cleanup = async (error?: Error): Promise<Response> => { // 1. 获取文件锁
this.releaseFileLock(filePath) if (!(await this.acquireFileLock(filePath))) {
if (error) { return new Response("File is locked", { status: 423 })
await this.logAccess("READ", filePath, false, error.message) }
return new Response("Internal Server Error", { status: 500 })
}
return new Response("OK", { status: 200 })
}
try { // 2. 确保目标目录存在
if (!(await this.acquireFileLock(filePath))) { await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
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 { // 3. 获取请求内容
const ext = path.extname(filePath).toLowerCase() const content = await request.text()
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> { // 4. 验证内容大小
try { if (content.length > this.MAX_FILE_SIZE) {
const stats = await fs.promises.lstat(filePath) return cleanup(new Error("Content too large"))
return stats.isSymbolicLink() }
} catch {
return false
}
}
private async checkFileSize(filePath: string): Promise<boolean> { // 5. 验证文件类型和内容
const ext = path.extname(filePath).toLowerCase()
if (ext === ".json") {
try { try {
const stats = await fs.promises.stat(filePath) JSON.parse(content)
return stats.size <= this.MAX_FILE_SIZE
} catch { } catch {
return false return cleanup(new Error("Invalid JSON content"))
} }
} }
private getPathsByOperation(operation: string): string[] | null { // 6. 写入文件
switch (operation) { await fs.promises.writeFile(filePath, content, "utf8")
case this.OPERATIONS.READ: await this.logAccess("WRITE", filePath, true)
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> { return cleanup()
const cleanup = async (error?: Error): Promise<Response> => { } catch (error) {
this.releaseFileLock(filePath) return cleanup(error as Error)
if (error) {
await this.logAccess("WRITE", filePath, false, error.message)
return new Response("Write failed: " + error.message, { status: 500 })
}
return new Response("Write successful", { status: 200 })
}
try {
// 1. 获取文件锁
if (!(await this.acquireFileLock(filePath))) {
return new Response("File is locked", { status: 423 })
}
// 2. 确保目标目录存在
await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
// 3. 获取请求内容
const content = await request.text()
// 4. 验证内容大小
if (content.length > this.MAX_FILE_SIZE) {
return cleanup(new Error("Content too large"))
}
// 5. 验证文件类型和内容
const ext = path.extname(filePath).toLowerCase()
if (ext === ".json") {
try {
JSON.parse(content)
} catch {
return cleanup(new Error("Invalid JSON content"))
}
}
// 6. 写入文件
await fs.promises.writeFile(filePath, content, "utf8")
await this.logAccess("WRITE", filePath, true)
return cleanup()
} catch (error) {
return cleanup(error as Error)
}
} }
}
} }
export default Zephyr export default Zephyr

28
src/main/utils/index.ts

@ -3,27 +3,27 @@ import { join } from "node:path"
import { webContents } from "electron" import { webContents } from "electron"
export function getFileUrl(app: string) { export function getFileUrl(app: string) {
let winURL = "" let winURL = ""
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
winURL = process.env["ELECTRON_RENDERER_URL"] + `/${app}#/` winURL = process.env["ELECTRON_RENDERER_URL"] + `/${app}#/`
} else { } else {
winURL = join(__dirname, `../renderer/${app}#/`) winURL = join(__dirname, `../renderer/${app}#/`)
} }
return slash(winURL) return slash(winURL)
} }
export function isPromise(value: () => any) { 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[]) => { 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) { export function slash(path: string) {
const isExtendedLengthPath = path.startsWith("\\\\?\\") const isExtendedLengthPath = path.startsWith("\\\\?\\")
if (isExtendedLengthPath) { if (isExtendedLengthPath) {
return path return path
} }
return path.replace(/\\/g, "/") return path.replace(/\\/g, "/")
} }

106
src/preload/call.ts

@ -2,70 +2,70 @@ import { ipcRenderer } from "electron"
let count = 0 let count = 0
export function call(command: string, ...args: any[]): Promise<any> { export function call(command: string, ...args: any[]): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!command) { if (!command) {
console.warn("命令不能为空") console.warn("命令不能为空")
return return
} }
count++ count++
const timestamp = new Date().getTime() const timestamp = new Date().getTime()
const key = timestamp + "-" + count const key = timestamp + "-" + count
let timeID: any = null let timeID: any = null
ipcRenderer.once(key, fn) ipcRenderer.once(key, fn)
function fn(_, err: any, res: any) { function fn(_, err: any, res: any) {
clearTimeout(timeID) clearTimeout(timeID)
if (err) { if (err) {
reject(err) reject(err)
return return
} }
resolve(res) resolve(res)
} }
ipcRenderer.send("command", key, command, ...args) ipcRenderer.send("command", key, command, ...args)
// 超过5s就取消监听 // 超过5s就取消监听
timeID = setTimeout(() => { timeID = setTimeout(() => {
reject(new Error(`超过5s未响应: ${command}`)) reject(new Error(`超过5s未响应: ${command}`))
ipcRenderer.removeListener(key, fn) ipcRenderer.removeListener(key, fn)
}, 5000) }, 5000)
}) })
} }
export function callLong(command: string, ...args: any[]): Promise<any> { export function callLong(command: string, ...args: any[]): Promise<any> {
return new Promise((resolve, reject) => { 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[]) {
if (!command) { if (!command) {
console.warn("命令不能为空") console.warn("命令不能为空")
return return
} }
count++ count++
const timestamp = new Date().getTime() const timestamp = new Date().getTime()
const key = timestamp + "-" + count const key = timestamp + "-" + count
const result = ipcRenderer.sendSync("command", key, command, ...args) ipcRenderer.once(key, fn)
if (!result) {
function fn(_, err: any, res: any) {
if (err) {
reject(err)
return 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 { call, callLong, callSync } from "./call"
import { IPopupMenuOption } from "#/popup-menu" import { IPopupMenuOption } from "#/popup-menu"
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const initStyle = document.createElement("style") const initStyle = document.createElement("style")
initStyle.textContent = ` initStyle.textContent = `
*, *,
*::before, *::before,
*::after { *::after {
@ -19,59 +19,59 @@ body {
// background: #F8F8F8; // background: #F8F8F8;
} }
` `
document.head.appendChild(initStyle) document.head.appendChild(initStyle)
}) })
// Custom APIs for renderer // Custom APIs for renderer
const api = { const api = {
call, call,
callLong, callLong,
callSync, callSync,
send(command: string, ...argu: any[]) { send(command: string, ...argu: any[]) {
if (!command) return if (!command) return
return ipcRenderer.send(command, ...argu) return ipcRenderer.send(command, ...argu)
}, },
sendSync(command: string, ...argu: any[]) { sendSync(command: string, ...argu: any[]) {
if (!command) return if (!command) return
return ipcRenderer.sendSync(command, ...argu) return ipcRenderer.sendSync(command, ...argu)
}, },
on(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) { on(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.on(command, cb) ipcRenderer.on(command, cb)
return () => ipcRenderer.removeListener(command, cb) return () => ipcRenderer.removeListener(command, cb)
}, },
once(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) { once(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.once(command, cb) ipcRenderer.once(command, cb)
return () => ipcRenderer.removeListener(command, cb) return () => ipcRenderer.removeListener(command, cb)
}, },
off(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) { off(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
return ipcRenderer.removeListener(command, cb) return ipcRenderer.removeListener(command, cb)
}, },
offAll(command: string) { offAll(command: string) {
return ipcRenderer.removeAllListeners(command) return ipcRenderer.removeAllListeners(command)
}, },
popupMenu(options: IPopupMenuOption) { popupMenu(options: IPopupMenuOption) {
ipcRenderer.send("x_popup_menu", curWebContentName, options) ipcRenderer.send("x_popup_menu", curWebContentName, options)
}, },
} }
let curWebContentName = "" let curWebContentName = ""
ipcRenderer.once("bind-window-manager", (_, name: string) => { ipcRenderer.once("bind-window-manager", (_, name: string) => {
curWebContentName = name curWebContentName = name
}) })
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise // renderer only if context isolation is enabled, otherwise
// just add to the DOM global. // just add to the DOM global.
if (process.contextIsolated) { if (process.contextIsolated) {
try { try {
contextBridge.exposeInMainWorld("electron", electronAPI) contextBridge.exposeInMainWorld("electron", electronAPI)
contextBridge.exposeInMainWorld("api", api) contextBridge.exposeInMainWorld("api", api)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} else { } else {
// @ts-ignore (define in dts) // @ts-ignore (define in dts)
window.electron = electronAPI window.electron = electronAPI
// @ts-ignore (define in dts) // @ts-ignore (define in dts)
window.api = api window.api = api
} }

48
src/renderer/about.html

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

1175
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> <!doctype html>
<html> <html>
<head>
<head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Electron</title> <title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <!-- 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:; script-src 'self' api:;
style-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
img-src 'self' data: *;" /> img-src 'self' data: *;"
</head> />
</head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<noscript> <noscript>
<style> <style>
[data-simplebar] { [data-simplebar] {
overflow: auto; overflow: auto;
} }
</style> </style>
</noscript> </noscript>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

32
src/renderer/src/App.vue

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

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

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

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

@ -1,44 +1,44 @@
*, *,
*::before, *::before,
*::after { *::after {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
font-weight: normal; font-weight: normal;
} }
html { html {
--text-normal: #6b6b6b; --text-normal: #6b6b6b;
--text-hover: #000000; --text-hover: #000000;
height: 100%; height: 100%;
} }
body { body {
--at-apply: text-normal; --at-apply: text-normal;
height: 100%; height: 100%;
} }
#app { #app {
height: 100%; height: 100%;
} }
* { * {
user-select: none; user-select: none;
outline: none; outline: none;
} }
.simplebar-scrollbar::before { .simplebar-scrollbar::before {
background-color: #bdbdbd; background-color: #bdbdbd;
border-radius: 0; border-radius: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
top: 0; top: 0;
} }
.simplebar-hover .simplebar-scrollbar::before { .simplebar-hover .simplebar-scrollbar::before {
background-color: #909090; background-color: #909090;
} }
.simplebar-wrapper:hover ~ .simplebar-track > .simplebar-scrollbar:before { .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 type OffFunction = () => void
export class PopupMenu { export class PopupMenu {
private _id: string private _id: string
private _items: IMenuItemOption[] private _items: IMenuItemOption[]
private _offs: any[] = [] private _offs: any[] = []
constructor(menu_items: IMenuItemOption[]) { constructor(menu_items: IMenuItemOption[]) {
this._id = `popup_menu_${Math.floor(Math.random() * 1e8)}` this._id = `popup_menu_${Math.floor(Math.random() * 1e8)}`
this._items = menu_items this._items = menu_items
} }
show(popupOptions?: PopupOptions) { show(popupOptions?: PopupOptions) {
// console.log('show') // console.log('show')
this.onHide() this.onHide()
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this const that = this
function readMenu(_items: IMenuItemOption[]) { function readMenu(_items: IMenuItemOption[]) {
return _items.map(i => { return _items.map(i => {
const d = { ...i } const d = { ...i }
if (typeof d.click === "function") { if (typeof d.click === "function") {
const r = Math.floor(Math.random() * 1e8) const r = Math.floor(Math.random() * 1e8)
const evt = `popup_menu_item_${_idx++}_${r}` const evt = `popup_menu_item_${_idx++}_${r}`
const off = api.once(evt, d.click as any) const off = api.once(evt, d.click as any)
that._offs.push(off) that._offs.push(off)
d._click_evt = evt d._click_evt = evt
delete d.click delete d.click
}
if (d.submenu && Array.isArray(d.submenu)) {
d.submenu = readMenu(d.submenu)
}
return d
})
} }
const items = readMenu(this._items) if (d.submenu && Array.isArray(d.submenu)) {
d.submenu = readMenu(d.submenu)
// popupOptions 中的 x,y 必须为整数 }
api.popupMenu({ return d
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)
} }
const items = readMenu(this._items)
private onHide() { // popupOptions 中的 x,y 必须为整数
// console.log('hide...') api.popupMenu({
this._offs.map(o => o()) menu_id: this._id,
this._offs = [] 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> <template>
<div ref="adjustLineEL" :class="['adjust-line', `adjust-line--${direction}`, { 'adjust-line--dragging': isDragging }]"> <div ref="adjustLineEL" :class="['adjust-line', `adjust-line--${direction}`, { 'adjust-line--dragging': isDragging }]">
<div class="adjust-line__handle"> <div class="adjust-line__handle">
<div class="adjust-line__grip"> <div class="adjust-line__grip">
<span class="grip-line"></span> <span class="grip-line"></span>
<span class="grip-line"></span> <span class="grip-line"></span>
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { nextTick, onMounted, ref, watch, computed, onBeforeUnmount, onErrorCaptured } from "vue" import { nextTick, onMounted, ref, watch, computed, onBeforeUnmount, onErrorCaptured } from "vue"
import { useDebounceFn } from "@vueuse/core" 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 // Props
interface AdjustLineProps { interface AdjustLineProps {
/** /**
* 所在方向 'left' | 'right' | 'top' | 'bottom' * 所在方向 'left' | 'right' | 'top' | 'bottom'
*/ */
@ -40,106 +40,106 @@ interface AdjustLineProps {
maxSize?: number maxSize?: number
defaultSize?: number defaultSize?: number
onChange?: (size: number) => void onChange?: (size: number) => void
} }
const props = withDefaults(defineProps<AdjustLineProps>(), { const props = withDefaults(defineProps<AdjustLineProps>(), {
direction: "right", direction: "right",
minSize: 100, minSize: 100,
maxSize: 800, maxSize: 800,
}) })
// //
const emit = defineEmits<{ const emit = defineEmits<{
(e: "resize", size: number): void (e: "resize", size: number): void
(e: "resizeStart"): void (e: "resizeStart"): void
(e: "resizeEnd", size: number): void (e: "resizeEnd", size: number): void
}>() }>()
let curTarget: HTMLElement | undefined | null let curTarget: HTMLElement | undefined | null
const isDragging = ref(false) const isDragging = ref(false)
const currentSize = ref(props.defaultSize || 0) const currentSize = ref(props.defaultSize || 0)
// 使computed // 使computed
const isHorizontal = computed(() => props.direction === "left" || props.direction === "right") const isHorizontal = computed(() => props.direction === "left" || props.direction === "right")
// // 使computed // // 使computed
// const cursorStyle = computed(() => (isHorizontal.value ? "ew-resize" : "ns-resize")) // const cursorStyle = computed(() => (isHorizontal.value ? "ew-resize" : "ns-resize"))
// // localStorage // // localStorage
// const storageKey = computed(() => `adjust-line-${props.mid}`) // const storageKey = computed(() => `adjust-line-${props.mid}`)
// function saveSize(size: number) { // function saveSize(size: number) {
// if (props.mid) { // if (props.mid) {
// try { // try {
// localStorage.setItem(storageKey.value, String(size)) // localStorage.setItem(storageKey.value, String(size))
// } catch (error) { // } catch (error) {
// console.warn("Failed to save size to localStorage:", error) // console.warn("Failed to save size to localStorage:", error)
// } // }
// } // }
// } // }
// function loadSavedSize(): number | null { // function loadSavedSize(): number | null {
// if (props.mid) { // if (props.mid) {
// try { // try {
// const saved = localStorage.getItem(storageKey.value) // const saved = localStorage.getItem(storageKey.value)
// return saved ? Number(saved) : null // return saved ? Number(saved) : null
// } catch (error) { // } catch (error) {
// console.warn("Failed to load size from localStorage:", error) // console.warn("Failed to load size from localStorage:", error)
// return null // return null
// } // }
// } // }
// return null // return null
// } // }
// 使resize // 使resize
const emitResize = useDebounceFn((size: number) => { const emitResize = useDebounceFn((size: number) => {
emit("resize", size) emit("resize", size)
}, 16) }, 16)
// 使ResizeObserver // 使ResizeObserver
let observer: ResizeObserver | null = null let observer: ResizeObserver | null = null
const observeResize = () => { const observeResize = () => {
if (!adjustLineEL.value) return if (!adjustLineEL.value) return
observer = new ResizeObserver(() => { observer = new ResizeObserver(() => {
if (curTarget) { if (curTarget) {
const size = isHorizontal.value ? curTarget.clientWidth : curTarget.clientHeight const size = isHorizontal.value ? curTarget.clientWidth : curTarget.clientHeight
currentSize.value = size currentSize.value = size
emitResize(size) emitResize(size)
} }
}) })
observer.observe(adjustLineEL.value) observer.observe(adjustLineEL.value)
} }
onBeforeUnmount(() => { onBeforeUnmount(() => {
observer && observer.disconnect() observer && observer.disconnect()
}) })
onMounted(async () => { onMounted(async () => {
await nextTick() await nextTick()
if (!props.target) { if (!props.target) {
curTarget = adjustLineEL.value?.parentElement curTarget = adjustLineEL.value?.parentElement
} else { } else {
curTarget = props.target curTarget = props.target
} }
if (curTarget) { if (curTarget) {
handle(curTarget) handle(curTarget)
} }
watch( watch(
() => props.target, () => props.target,
target => { target => {
curTarget = target curTarget = target
if (curTarget) { if (curTarget) {
handle(curTarget) handle(curTarget)
} }
}, },
) )
observeResize() observeResize()
}) })
function handle(target: HTMLElement) { function handle(target: HTMLElement) {
if (!adjustLineEL.value) return if (!adjustLineEL.value) return
const nextContainer = target const nextContainer = target
const el = adjustLineEL.value const el = adjustLineEL.value
@ -148,368 +148,368 @@ function handle(target: HTMLElement) {
const watchContainer = props.watch const watchContainer = props.watch
let isThree = false let isThree = false
if (container !== nextContainer) { if (container !== nextContainer) {
isThree = true isThree = true
} }
if (nextContainer && el && container && parentContainer) { if (nextContainer && el && container && parentContainer) {
if (props.direction === "left" || props.direction === "right") { if (props.direction === "left" || props.direction === "right") {
if (props.mid) { if (props.mid) {
let w = localStorage.getItem(props.mid) let w = localStorage.getItem(props.mid)
if (w != undefined) { if (w != undefined) {
container.style.width = w + "px" 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) { if (props.direction == "right") {
let width = container.clientWidth w = width - offset
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 (w >= owidth) {
if (props.direction === "top" || props.direction === "bottom") { 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) { if (props.mid) {
let w = localStorage.getItem(props.mid) let width = container.clientWidth
if (w != undefined) { localStorage.setItem(props.mid, String(width))
container.style.height = w + "px"
}
} }
el.onmousedown = function (e) { }
let height = container.clientHeight }
let nheight = nextContainer.clientHeight }
// let oheight = nheight + height if (props.direction === "top" || props.direction === "bottom") {
let oheight = parentContainer.clientHeight if (props.mid) {
let hheight = watchContainer?.clientHeight ?? 0 let w = localStorage.getItem(props.mid)
if (isThree) { if (w != undefined) {
oheight = nheight + height container.style.height = w + "px"
} }
}
let startY = e.clientY el.onmousedown = function (e) {
let height = container.clientHeight
let lastPointerEvents = document.body.style.pointerEvents let nheight = nextContainer.clientHeight
let lastUserSelect = document.body.style.userSelect // let oheight = nheight + height
let lastOnmousemove = document.onmousemove let oheight = parentContainer.clientHeight
let lastOnmouseup = document.onmouseup let hheight = watchContainer?.clientHeight ?? 0
if (isThree) {
document.onmousemove = function (e) { oheight = nheight + height
let nowY = e.clientY }
let h = 0
let offset = startY - nowY let startY = e.clientY
if (props.direction == "top") {
h = height + startY - nowY let lastPointerEvents = document.body.style.pointerEvents
} let lastUserSelect = document.body.style.userSelect
if (props.direction == "bottom") { let lastOnmousemove = document.onmousemove
h = height - offset let lastOnmouseup = document.onmouseup
}
console.log(oheight) document.onmousemove = function (e) {
let nowY = e.clientY
if (h >= oheight) { let h = 0
h = oheight let offset = startY - nowY
} if (props.direction == "top") {
if (h <= 0) { h = height + startY - nowY
h = 0 }
} if (props.direction == "bottom") {
// if (Math.abs(h - oheight / 2) <= 15) { h = height - offset
// h = oheight / 2 }
// } console.log(oheight)
// if (Math.abs(h - oheight) < 50) {
// h = oheight if (h >= oheight) {
// } 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 (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) { // function handleDrag(e: MouseEvent, target: HTMLElement) {
// const startPos = isHorizontal.value ? e.clientX : e.clientY // const startPos = isHorizontal.value ? e.clientX : e.clientY
// const startSize = isHorizontal.value ? target.clientWidth : target.clientHeight // const startSize = isHorizontal.value ? target.clientWidth : target.clientHeight
// const handleMouseMove = (e: MouseEvent) => { // const handleMouseMove = (e: MouseEvent) => {
// const currentPos = isHorizontal.value ? e.clientX : e.clientY // const currentPos = isHorizontal.value ? e.clientX : e.clientY
// const diff = props.direction === "right" || props.direction === "bottom" ? startPos - currentPos : currentPos - startPos // const diff = props.direction === "right" || props.direction === "bottom" ? startPos - currentPos : currentPos - startPos
// let newSize = startSize - diff // let newSize = startSize - diff
// // // //
// newSize = Math.max(props.minSize, Math.min(props.maxSize, newSize)) // newSize = Math.max(props.minSize, Math.min(props.maxSize, newSize))
// // // //
// if (isHorizontal.value) { // if (isHorizontal.value) {
// target.style.width = `${newSize}px` // target.style.width = `${newSize}px`
// } else { // } else {
// target.style.height = `${newSize}px` // target.style.height = `${newSize}px`
// } // }
// currentSize.value = newSize // currentSize.value = newSize
// emit("resize", newSize) // emit("resize", newSize)
// } // }
// const handleMouseUp = () => { // const handleMouseUp = () => {
// document.removeEventListener("mousemove", handleMouseMove) // document.removeEventListener("mousemove", handleMouseMove)
// document.removeEventListener("mouseup", handleMouseUp) // document.removeEventListener("mouseup", handleMouseUp)
// document.body.style.userSelect = "" // document.body.style.userSelect = ""
// isDragging.value = false // isDragging.value = false
// saveSize(currentSize.value) // saveSize(currentSize.value)
// emit("resizeEnd", currentSize.value) // emit("resizeEnd", currentSize.value)
// } // }
// document.addEventListener("mousemove", handleMouseMove) // document.addEventListener("mousemove", handleMouseMove)
// document.addEventListener("mouseup", handleMouseUp) // document.addEventListener("mouseup", handleMouseUp)
// document.body.style.userSelect = "none" // document.body.style.userSelect = "none"
// isDragging.value = true // isDragging.value = true
// emit("resizeStart") // emit("resizeStart")
// } // }
const debug = { const debug = {
log: (...args: any[]) => { log: (...args: any[]) => {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
console.log("[AdjustLine]", ...args) console.log("[AdjustLine]", ...args)
} }
}, },
error: (...args: any[]) => { 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) debug.error(`Error in ${context}:`, error)
// //
} }
// //
onErrorCaptured((err, instance, info) => { onErrorCaptured((err, instance, info) => {
handleError(err as Error, info) handleError(err as Error, info)
console.log(instance); console.log(instance)
return false return false
}) })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.adjust-line { .adjust-line {
position: absolute; position: absolute;
z-index: 999; z-index: 999;
&__handle { &__handle {
position: absolute; position: absolute;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: transparent; background: transparent;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
&__grip { &__grip {
display: flex; display: flex;
gap: 3px; gap: 3px;
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
.adjust-line:hover &, .adjust-line:hover &,
.adjust-line--dragging & { .adjust-line--dragging & {
opacity: 1; opacity: 1;
} }
} }
.grip-line { .grip-line {
background-color: #999; background-color: #999;
border-radius: 1px; border-radius: 1px;
.adjust-line:hover &, .adjust-line:hover &,
.adjust-line--dragging & { .adjust-line--dragging & {
background-color: #666; background-color: #666;
} }
} }
// 线 // 线
&--left, &--left,
&--right { &--right {
top: 0;
bottom: 0;
width: 10px; //
cursor: col-resize;
.adjust-line__handle {
left: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 10px; // width: 100%;
cursor: col-resize; }
.adjust-line__handle {
left: 0;
top: 0;
bottom: 0;
width: 100%;
}
.adjust-line__grip { .adjust-line__grip {
flex-direction: column; flex-direction: column;
} }
.grip-line { .grip-line {
width: 2px; width: 2px;
height: 16px; height: 16px;
} }
&:hover .adjust-line__handle { &:hover .adjust-line__handle {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
} }
// 线 // 线
&--top, &--top,
&--bottom { &--bottom {
left: 0;
right: 0;
height: 10px; //
cursor: row-resize;
.adjust-line__handle {
top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 10px; // height: 100%;
cursor: row-resize; }
.adjust-line__handle {
top: 0;
left: 0;
right: 0;
height: 100%;
}
.adjust-line__grip { .adjust-line__grip {
flex-direction: row; flex-direction: row;
} }
.grip-line { .grip-line {
width: 16px; width: 16px;
height: 2px; height: 2px;
} }
&:hover .adjust-line__handle { &:hover .adjust-line__handle {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
} }
// //
&--left { &--left {
left: -5px; left: -5px;
} }
&--right { &--right {
right: -5px; right: -5px;
} }
&--top { &--top {
top: -5px; top: -5px;
} }
&--bottom { &--bottom {
bottom: -5px; bottom: -5px;
} }
// //
&--dragging { &--dragging {
&::after { &::after {
content: ""; content: "";
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: -1; z-index: -1;
cursor: inherit; cursor: inherit;
} }
.adjust-line__handle { .adjust-line__handle {
background-color: rgba(0, 0, 0, 0.08); background-color: rgba(0, 0, 0, 0.08);
} }
.grip-line { .grip-line {
background-color: #666; background-color: #666;
} }
} }
} }
</style> </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 * 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 { export class PlaceholderContentWidget implements monaco.editor.IContentWidget {
private static readonly ID = "editor.widget.placeholderHint" private static readonly ID = "editor.widget.placeholderHint"
private domNode: HTMLElement | undefined private domNode: HTMLElement | undefined
constructor( constructor(
private readonly placeholder: string, private readonly placeholder: string,
private readonly editor: monaco.editor.ICodeEditor, private readonly editor: monaco.editor.ICodeEditor,
) { ) {
// register a listener for editor code changes // register a listener for editor code changes
editor.onDidChangeModelContent(() => this.onDidChangeModelContent()) editor.onDidChangeModelContent(() => this.onDidChangeModelContent())
// ensure that on initial load the placeholder is shown // ensure that on initial load the placeholder is shown
this.onDidChangeModelContent() 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() === "") { getId(): string {
this.editor.addContentWidget(this) return PlaceholderContentWidget.ID
} else { }
this.editor.removeContentWidget(this)
} 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 this.domNode
return PlaceholderContentWidget.ID }
}
getDomNode(): HTMLElement { getPosition(): monaco.editor.IContentWidgetPosition | null {
if (!this.domNode) { return {
this.domNode = document.createElement("div") position: { lineNumber: 1, column: 1 },
this.domNode.style.width = "max-content" preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
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 { dispose(): void {
return { this.editor.removeContentWidget(this)
position: { lineNumber: 1, column: 1 }, }
preference: [monaco.editor.ContentWidgetPositionPreference.EXACT],
}
}
dispose(): void {
this.editor.removeContentWidget(this)
}
} }

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

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

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

@ -1,32 +1,32 @@
export function judgeFile(filename: string) { export function judgeFile(filename: string) {
if (!filename) return if (!filename) return
let ext = [ let ext = [
{ language: "vue", ext: ".vue", index: -1 }, { language: "vue", ext: ".vue", index: -1 },
{ language: "javascript", ext: ".js", index: -1 }, { language: "javascript", ext: ".js", index: -1 },
{ language: "css", ext: ".css", index: -1 }, { language: "css", ext: ".css", index: -1 },
{ language: "html", ext: ".html", index: -1 }, { language: "html", ext: ".html", index: -1 },
{ language: "tsx", ext: ".tsx", index: -1 }, { language: "tsx", ext: ".tsx", index: -1 },
{ language: "typescript", ext: ".ts", index: -1 }, { language: "typescript", ext: ".ts", index: -1 },
{ language: "markdown", ext: ".md", index: -1 }, { language: "markdown", ext: ".md", index: -1 },
{ language: "json", ext: ".json", index: -1 }, { language: "json", ext: ".json", index: -1 },
{ language: "web", ext: ".web", index: -1 }, { language: "web", ext: ".web", index: -1 },
{ language: "dot", pre: ".", index: -1 }, { language: "dot", pre: ".", index: -1 },
] ]
let cur let cur
for (let i = 0; i < ext.length; i++) { for (let i = 0; i < ext.length; i++) {
const e = ext[i] const e = ext[i]
if (e.ext && filename.endsWith(e.ext)) { if (e.ext && filename.endsWith(e.ext)) {
let index = filename.lastIndexOf(e.ext) let index = filename.lastIndexOf(e.ext)
e.index = index e.index = index
cur = e cur = e
break break
}
if (e.pre && filename.startsWith(e.pre)) {
let index = filename.indexOf(e.pre)
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> <template>
<div <div
relative relative
h="30px" h="30px"
leading="29px" leading="29px"
pr="137px" pr="137px"
:style="{ paddingRight: isFullScreen ? '0' : '' }" :style="{ paddingRight: isFullScreen ? '0' : '' }"
select-none select-none
border-b="1px solid #E5E5E5" border-b="1px solid #E5E5E5"
bg="#F8F8F8" bg="#F8F8F8"
> >
<div absolute top-0 right-0 bottom-0 left-0 style="-webkit-app-region: drag"></div> <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 h-full px-2 flex items-center gap-1 justify-between>
<div flex items-center gap-1> <div flex items-center gap-1>
<img w="16px" h="16px" :src="icon" /> <img w="16px" h="16px" :src="icon" />
<div relative h-full inline-flex items-center text-sm>{{ config.app_title }}</div> <div relative h-full inline-flex items-center text-sm>{{ config.app_title }}</div>
<div relative class="list"> <div relative class="list">
<div class="item" @click="onClickMenu">{{ t("caidan") }}</div> <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>
</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>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import icon from "@res/icon.png" import icon from "@res/icon.png"
import config from "config" import config from "config"
import { PopupMenu } from "@/bridge/PopupMenu" import { PopupMenu } from "@/bridge/PopupMenu"
import { usePlatForm } from "common/usePlatform" import { usePlatForm } from "common/usePlatform"
const { PlatForm } = usePlatForm() const { PlatForm } = usePlatForm()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const isFullScreen = ref(false) const isFullScreen = ref(false)
onBeforeMount(async () => { onBeforeMount(async () => {
isFullScreen.value = await PlatForm.isFullScreen() isFullScreen.value = await PlatForm.isFullScreen()
}) })
const isHome = computed(() => { const isHome = computed(() => {
if (route.fullPath === "/") { if (route.fullPath === "/") {
return true return true
} }
return false return false
}) })
function back() { function back() {
router.push("/") router.push("/")
} }
const { t } = useI18n() const { t } = useI18n()
const onClickMenu = e => { const onClickMenu = e => {
const menu = new PopupMenu([ const menu = new PopupMenu([
{ {
label: isFullScreen.value ? t("qu-xiao-quan-ping") : t("quan-ping"), label: isFullScreen.value ? t("qu-xiao-quan-ping") : t("quan-ping"),
async click() { async click() {
await PlatForm.toggleFullScreen() await PlatForm.toggleFullScreen()
isFullScreen.value = !isFullScreen.value isFullScreen.value = !isFullScreen.value
},
}, },
{ },
label: t("qie-huan-kai-fa-zhe-gong-ju"), {
async click() { label: t("qie-huan-kai-fa-zhe-gong-ju"),
PlatForm.toggleDevTools() async click() {
}, PlatForm.toggleDevTools()
}, },
},
]) ])
const obj = e.target.getBoundingClientRect() const obj = e.target.getBoundingClientRect()
menu.show({ x: ~~obj.x, y: ~~(obj.y + obj.height) }) menu.show({ x: ~~obj.x, y: ~~(obj.y + obj.height) })
} }
const onClickAbout = () => { const onClickAbout = () => {
PlatForm.showAbout() PlatForm.showAbout()
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.list { .list {
@apply: flex gap="5px"; @apply: flex gap="5px";
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
.item { .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> </style>

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

@ -1,13 +1,13 @@
<script setup lang="ts"> <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> </script>
<template> <template>
<ul class="versions"> <ul class="versions">
<li class="electron-version">Electron v{{ versions.electron }}</li> <li class="electron-version">Electron v{{ versions.electron }}</li>
<li class="chrome-version">Chromium v{{ versions.chrome }}</li> <li class="chrome-version">Chromium v{{ versions.chrome }}</li>
<li class="node-version">Node v{{ versions.node }}</li> <li class="node-version">Node v{{ versions.node }}</li>
</ul> </ul>
</template> </template>

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

@ -1,3 +1,3 @@
export function useTest() { 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" /> /// <reference types="unplugin-vue-router/client" />
declare module "*.vue" { declare module "*.vue" {
import type { DefineComponent } from "vue" import type { DefineComponent } from "vue"
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any> const component: DefineComponent<{}, {}, any>
export default component export default component
} }

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

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

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

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

8
src/renderer/src/main.ts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -5,8 +5,8 @@ import { setupLayouts } from "virtual:generated-layouts"
const routes = setupLayouts(generatedRoutes) const routes = setupLayouts(generatedRoutes)
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes, routes,
}) })
export { router } export { router }
@ -14,5 +14,5 @@ export { router }
export default router export default router
if (import.meta.hot) { 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" import type { AttributifyAttributes } from "@unocss/preset-attributify"
declare module "@vue/runtime-dom" { 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 * @returns
*/ */
export const getAssetsFile = url => { export const getAssetsFile = url => {
const urlArr = String(url).split("/") const urlArr = String(url).split("/")
const prefix = urlArr.slice(-2)[0] const prefix = urlArr.slice(-2)[0]
const fileName = urlArr.slice(-1)[0] const fileName = urlArr.slice(-1)[0]
return new URL(`../assets/images/${prefix}/${fileName}`, import.meta.url).href return new URL(`../assets/images/${prefix}/${fileName}`, import.meta.url).href
} }

25
src/types/global.d.ts

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

10
src/types/popup-menu.ts

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

136
uno.config.ts

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

2
vue-macros.config.ts

@ -1,4 +1,4 @@
import { defineConfig } from "unplugin-vue-macros" import { defineConfig } from "unplugin-vue-macros"
export default defineConfig({ 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 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虚拟桌面开发遇坑之透明窗口鼠标穿透 electron+vue虚拟桌面开发遇坑之透明窗口鼠标穿透
https://blog.csdn.net/weixin_42421494/article/details/102800491 https://blog.csdn.net/weixin_42421494/article/details/102800491
截图 截图
https://zhuanlan.zhihu.com/p/46043613?from_voters_page=true https://zhuanlan.zhihu.com/p/46043613?from_voters_page=true

Loading…
Cancel
Save