From 32fcfa3e86cfb532ee34fe0160bb195ac7eab04d Mon Sep 17 00:00:00 2001 From: dash <1549469775@qq.com> Date: Thu, 30 Oct 2025 01:48:28 +0800 Subject: [PATCH] Initialize project structure with configuration files, add TRPC support, and implement window management. Introduce new packages for config, logger, and TRPC, along with necessary updates to existing files for improved functionality and organization. --- .env | 0 .npmrc | 1 + .vscode/settings.json | 2 +- electron.vite.config.ts | 16 +- package.json | 8 +- packages/config/app_config.json | 11 + packages/config/exe_config.json | 16 ++ packages/config/index.ts | 18 ++ packages/config/package.json | 12 + packages/logger/index.ts | 0 packages/logger/package.json | 12 + packages/trpc/common/app.ts | 8 + packages/trpc/common/index.ts | 3 + packages/trpc/common/routers/post.ts | 19 ++ packages/trpc/common/routers/user.ts | 38 +++ packages/trpc/common/trpc.ts | 8 + packages/trpc/main/index.ts | 11 + packages/trpc/package.json | 12 + packages/trpc/preload/index.ts | 5 + packages/trpc/renderer/index.ts | 9 + pnpm-lock.yaml | 69 ++++- pnpm-workspace.yaml | 2 + src/main/api.ts | 34 --- src/main/env.d.ts | 9 + src/main/index.ts | 108 +++----- src/main/modules/window-manager/index.ts | 359 ++++++++++++++++++++++++++ src/main/modules/window-manager/windowsMap.ts | 107 ++++++++ src/main/utils/base/base-singleton.ts | 30 +++ src/main/utils/file.ts | 18 ++ src/main/utils/url.ts | 8 + src/preload/index.ts | 27 +- src/renderer/about.html | 32 +++ src/renderer/src/App.vue | 13 +- src/renderer/src/about.ts | 8 + src/renderer/src/trpc.ts | 7 - tsconfig.node.json | 24 +- tsconfig.web.json | 3 +- 37 files changed, 915 insertions(+), 152 deletions(-) create mode 100644 .env create mode 100644 packages/config/app_config.json create mode 100644 packages/config/exe_config.json create mode 100644 packages/config/index.ts create mode 100644 packages/config/package.json create mode 100644 packages/logger/index.ts create mode 100644 packages/logger/package.json create mode 100644 packages/trpc/common/app.ts create mode 100644 packages/trpc/common/index.ts create mode 100644 packages/trpc/common/routers/post.ts create mode 100644 packages/trpc/common/routers/user.ts create mode 100644 packages/trpc/common/trpc.ts create mode 100644 packages/trpc/main/index.ts create mode 100644 packages/trpc/package.json create mode 100644 packages/trpc/preload/index.ts create mode 100644 packages/trpc/renderer/index.ts create mode 100644 pnpm-workspace.yaml delete mode 100644 src/main/api.ts create mode 100644 src/main/env.d.ts create mode 100644 src/main/modules/window-manager/index.ts create mode 100644 src/main/modules/window-manager/windowsMap.ts create mode 100644 src/main/utils/base/base-singleton.ts create mode 100644 src/main/utils/file.ts create mode 100644 src/main/utils/url.ts create mode 100644 src/renderer/about.html create mode 100644 src/renderer/src/about.ts delete mode 100644 src/renderer/src/trpc.ts diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/.npmrc b/.npmrc index 34862ff..3b4920e 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ electron_mirror=https://npmmirror.com/mirrors/electron/ electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ +link-workspace-packages=true diff --git a/.vscode/settings.json b/.vscode/settings.json index 4c05394..20951e4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 470c4a0..f5ce31b 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -4,6 +4,12 @@ import vue from '@vitejs/plugin-vue' export default defineConfig({ main: { + resolve: { + alias: { + main: resolve('src/main'), + "@res": resolve("resources"), + } + }, plugins: [externalizeDepsPlugin()] }, preload: { @@ -15,6 +21,14 @@ export default defineConfig({ '@renderer': resolve('src/renderer/src') } }, - plugins: [vue()] + plugins: [vue()], + build: { + rollupOptions: { + input: { + main: resolve(__dirname, "./src/renderer/index.html"), + about: resolve(__dirname, "./src/renderer/about.html"), + }, + }, + }, } }) diff --git a/package.json b/package.json index 3d9cce3..0c98799 100644 --- a/package.json +++ b/package.json @@ -25,22 +25,28 @@ "@electron-toolkit/utils": "^4.0.0", "@trpc/client": "^10.45.2", "@trpc/server": "^10.45.2", - "electron-trpc": "^0.7.1", "electron-updater": "^6.3.9", + "superjson": "^2.2.5", "zod": "^4.1.12" }, "devDependencies": { "@electron-toolkit/eslint-config-prettier": "3.0.0", "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/tsconfig": "^2.0.0", + "@types/lodash-es": "^4.17.12", "@types/node": "^22.18.6", "@vitejs/plugin-vue": "^6.0.1", + "config": "workspace:^", "electron": "^38.1.2", "electron-builder": "^25.1.8", + "electron-trpc": "^0.7.1", "electron-vite": "^4.0.1", "eslint": "^9.36.0", "eslint-plugin-vue": "^10.4.0", + "lodash-es": "^4.17.21", + "logger": "workspace:^", "prettier": "^3.6.2", + "trpc": "workspace:^", "typescript": "^5.9.2", "vite": "^7.1.6", "vue": "^3.5.21", diff --git a/packages/config/app_config.json b/packages/config/app_config.json new file mode 100644 index 0000000..fa54b1f --- /dev/null +++ b/packages/config/app_config.json @@ -0,0 +1,11 @@ +{ + "storagePath": "$storagePath$", + "language": "zh", + "dev:debug": 2, + "common.theme": "auto", + "update.hoturl": "https://alist.xieyaxin.top/d/%E8%B5%84%E6%BA%90/%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.zip?sign=eqy35CR-J1SOQZz0iUN2P3B0BiyZPdYH0362nLXbUhE=:1749085071", + "update.repo": "wood-desktop", + "update.owner": "npmrun", + "update.allowDowngrade": false, + "update.allowPrerelease": false +} diff --git a/packages/config/exe_config.json b/packages/config/exe_config.json new file mode 100644 index 0000000..14330f3 --- /dev/null +++ b/packages/config/exe_config.json @@ -0,0 +1,16 @@ +{ + "name": "zephyr", + "appId": "com.zephyr.app", + "win": { + "executableName": "zephyr" + }, + "linux": { + "target": ["AppImage", "snap", "deb"], + "maintainer": "electronjs.org", + "category": "Utility" + }, + "publish": { + "provider": "generic", + "url": "https://example.com/auto-updates" + } +} diff --git a/packages/config/index.ts b/packages/config/index.ts new file mode 100644 index 0000000..0888edd --- /dev/null +++ b/packages/config/index.ts @@ -0,0 +1,18 @@ +import AppConfig from "./app_config.json" +import ExeConfig from "./exe_config.json" + +// 定义主题类型 +type ThemeType = "light" | "dark" | "auto" +// 定义语言类型 +type LanguageType = "zh" | "en" + +export type IConfig = typeof AppConfig & + Pick, "common.theme"> & { + language: LanguageType + "common.theme": ThemeType + } + +export default { + appConfig: AppConfig, + exeConfig: ExeConfig, +} diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 0000000..2511d8a --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,12 @@ +{ + "name": "config", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/logger/index.ts b/packages/logger/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..04dba78 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,12 @@ +{ + "name": "logger", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/trpc/common/app.ts b/packages/trpc/common/app.ts new file mode 100644 index 0000000..a582e45 --- /dev/null +++ b/packages/trpc/common/app.ts @@ -0,0 +1,8 @@ +import { mergeRouters } from './trpc'; + +import { userRouter } from './routers/user'; +import { postRouter } from './routers/post'; + +export const appRouter = mergeRouters(userRouter, postRouter) + +export type AppRouter = typeof appRouter; \ No newline at end of file diff --git a/packages/trpc/common/index.ts b/packages/trpc/common/index.ts new file mode 100644 index 0000000..6f95be5 --- /dev/null +++ b/packages/trpc/common/index.ts @@ -0,0 +1,3 @@ +import type { AppRouter } from './app'; + +export { AppRouter }; \ No newline at end of file diff --git a/packages/trpc/common/routers/post.ts b/packages/trpc/common/routers/post.ts new file mode 100644 index 0000000..09ec489 --- /dev/null +++ b/packages/trpc/common/routers/post.ts @@ -0,0 +1,19 @@ +import { router, publicProcedure } from '../trpc'; +import { z } from 'zod'; + +export const postRouter = router({ + postCreate: publicProcedure + .input( + z.object({ + title: z.string(), + }), + ) + .mutation(() => { + // const { input } = opts; + // [...] + }), + postList: publicProcedure.query(() => { + // ... + return []; + }), +}); \ No newline at end of file diff --git a/packages/trpc/common/routers/user.ts b/packages/trpc/common/routers/user.ts new file mode 100644 index 0000000..4f1178e --- /dev/null +++ b/packages/trpc/common/routers/user.ts @@ -0,0 +1,38 @@ +import { router, publicProcedure } from '../trpc'; +import { observable } from '@trpc/server/observable'; +import { EventEmitter } from 'events'; +import { WindowManager } from 'main/modules/window-manager'; + +const ee = new EventEmitter(); + +export const userRouter = router({ + userList: publicProcedure.query(() => { + // [..] + ee.emit('greeting', `Greeted`) + return [ + { + id: 1, + name: 'John Doe', + windowsLength: WindowManager.getWindowsLength(), + }, + { + id: 2, + name: 'Jane Doe', + windowsLength: WindowManager.getWindowsLength(), + }, + ]; + }), + subscribeGreeting: publicProcedure.subscription(() => { + return observable<{ text: string }>((emit) => { + function onGreet(text: string) { + emit.next({ text }); + } + + ee.on('greeting', onGreet); + + return () => { + ee.off('greeting', onGreet); + }; + }); + }), +}); \ No newline at end of file diff --git a/packages/trpc/common/trpc.ts b/packages/trpc/common/trpc.ts new file mode 100644 index 0000000..c852ac8 --- /dev/null +++ b/packages/trpc/common/trpc.ts @@ -0,0 +1,8 @@ +import { initTRPC } from '@trpc/server'; +import superjson from 'superjson'; + +const t = initTRPC.create({ transformer: superjson, isServer: true }); + +export const mergeRouters = t.mergeRouters; +export const router = t.router; +export const publicProcedure = t.procedure; \ No newline at end of file diff --git a/packages/trpc/main/index.ts b/packages/trpc/main/index.ts new file mode 100644 index 0000000..5e6caa2 --- /dev/null +++ b/packages/trpc/main/index.ts @@ -0,0 +1,11 @@ +import { appRouter } from "../common/app"; +import { BrowserWindow } from "electron"; +import { createIPCHandler } from 'electron-trpc/main'; + +const handler = createIPCHandler({ router: appRouter, windows: []}); + +export function trpcBindWindows(windows: BrowserWindow[]) { + windows.forEach(window => { + handler.attachWindow(window); + }); +} \ No newline at end of file diff --git a/packages/trpc/package.json b/packages/trpc/package.json new file mode 100644 index 0000000..5501719 --- /dev/null +++ b/packages/trpc/package.json @@ -0,0 +1,12 @@ +{ + "name": "trpc", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/trpc/preload/index.ts b/packages/trpc/preload/index.ts new file mode 100644 index 0000000..fe8e82a --- /dev/null +++ b/packages/trpc/preload/index.ts @@ -0,0 +1,5 @@ +import { exposeElectronTRPC } from 'electron-trpc/main'; + +process.once('loaded', async () => { + exposeElectronTRPC(); +}); \ No newline at end of file diff --git a/packages/trpc/renderer/index.ts b/packages/trpc/renderer/index.ts new file mode 100644 index 0000000..758d7d3 --- /dev/null +++ b/packages/trpc/renderer/index.ts @@ -0,0 +1,9 @@ +import { createTRPCProxyClient } from '@trpc/client'; +import { ipcLink } from 'electron-trpc/renderer'; +import superjson from 'superjson'; +import type { AppRouter } from '../common'; + +export const client = createTRPCProxyClient({ + links: [ipcLink()], + transformer: superjson, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 900d746..89e63f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,12 +20,12 @@ importers: '@trpc/server': specifier: ^10.45.2 version: 10.45.2 - electron-trpc: - specifier: ^0.7.1 - version: 0.7.1(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(electron@38.4.0) electron-updater: specifier: ^6.3.9 version: 6.6.2 + superjson: + specifier: ^2.2.5 + version: 2.2.5 zod: specifier: ^4.1.12 version: 4.1.12 @@ -39,18 +39,27 @@ importers: '@electron-toolkit/tsconfig': specifier: ^2.0.0 version: 2.0.0(@types/node@22.18.12) + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/node': specifier: ^22.18.6 version: 22.18.12 '@vitejs/plugin-vue': specifier: ^6.0.1 version: 6.0.1(vite@7.1.12(@types/node@22.18.12))(vue@3.5.22(typescript@5.9.3)) + config: + specifier: workspace:^ + version: link:packages/config electron: specifier: ^38.1.2 version: 38.4.0 electron-builder: specifier: ^25.1.8 version: 25.1.8(electron-builder-squirrel-windows@25.1.8) + electron-trpc: + specifier: ^0.7.1 + version: 0.7.1(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(electron@38.4.0) electron-vite: specifier: ^4.0.1 version: 4.0.1(vite@7.1.12(@types/node@22.18.12)) @@ -60,9 +69,18 @@ importers: eslint-plugin-vue: specifier: ^10.4.0 version: 10.5.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0)(typescript@5.9.3))(eslint@9.38.0)(vue-eslint-parser@10.2.0(eslint@9.38.0)) + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + logger: + specifier: workspace:^ + version: link:packages/logger prettier: specifier: ^3.6.2 version: 3.6.2 + trpc: + specifier: workspace:^ + version: link:packages/trpc typescript: specifier: ^5.9.2 version: 5.9.3 @@ -79,6 +97,12 @@ importers: specifier: ^3.0.7 version: 3.1.2(typescript@5.9.3) + packages/config: {} + + packages/logger: {} + + packages/trpc: {} + packages: 7zip-bin@5.2.0: @@ -654,6 +678,12 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -1068,6 +1098,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -1661,6 +1695,10 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -1736,6 +1774,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -2269,6 +2310,10 @@ packages: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} + superjson@2.2.5: + resolution: {integrity: sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==} + engines: {node: '>=16'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3039,6 +3084,12 @@ snapshots: dependencies: '@types/node': 22.18.12 + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + '@types/ms@2.1.0': {} '@types/node@22.18.12': @@ -3596,6 +3647,10 @@ snapshots: convert-source-map@2.0.0: {} + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + core-util-is@1.0.2: optional: true @@ -4308,6 +4363,8 @@ snapshots: is-unicode-supported@0.1.0: {} + is-what@5.5.0: {} + isarray@1.0.0: {} isbinaryfile@4.0.10: {} @@ -4376,6 +4433,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.21: {} + lodash.defaults@4.2.0: {} lodash.difference@4.5.0: {} @@ -4916,6 +4975,10 @@ snapshots: transitivePeerDependencies: - supports-color + superjson@2.2.5: + dependencies: + copy-anything: 4.0.5 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..dee51e9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" diff --git a/src/main/api.ts b/src/main/api.ts deleted file mode 100644 index b9fb465..0000000 --- a/src/main/api.ts +++ /dev/null @@ -1,34 +0,0 @@ -import z from 'zod'; -import { initTRPC } from '@trpc/server'; -import { observable } from '@trpc/server/observable'; -import { EventEmitter } from 'events'; - -const ee = new EventEmitter(); - -const t = initTRPC.create({ isServer: true }); - -export const router = t.router({ - greeting: t.procedure.input(z.object({ name: z.string() })).query((req) => { - const { input } = req; - - ee.emit('greeting', `Greeted ${input.name}`); - return { - text: `Hello ${input.name}` as const, - }; - }), - subscript: t.procedure.subscription(() => { - return observable((emit) => { - function onGreet(text: string) { - emit.next({ text }); - } - - ee.on('greeting', onGreet); - - return () => { - ee.off('greeting', onGreet); - }; - }); - }), -}); - -export type AppRouter = typeof router; \ No newline at end of file diff --git a/src/main/env.d.ts b/src/main/env.d.ts new file mode 100644 index 0000000..929f119 --- /dev/null +++ b/src/main/env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + // readonly MAIN_VITE_DEBUG: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index ebbfe02..9d4fdc4 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,76 +1,44 @@ -import { app, shell, BrowserWindow, ipcMain } from 'electron' -import { join } from 'path' -import { electronApp, optimizer, is } from '@electron-toolkit/utils' -import icon from '../../resources/icon.png?asset' -import { createIPCHandler } from 'electron-trpc/main'; -import { router } from './api'; +import { app, ipcMain, nativeTheme } from 'electron' +import config from 'config' +import { electronApp, } from '@electron-toolkit/utils' +import WindowManager from './modules/window-manager'; +import icon from "@res/icon.png?asset" +import { getFileUrl, getPreloadUrl } from './utils/file'; -function createWindow(): void { - // Create the browser window. - const mainWindow = new BrowserWindow({ - width: 900, - height: 670, - show: false, - autoHideMenuBar: true, - ...(process.platform === 'linux' ? { icon } : {}), - webPreferences: { - preload: join(__dirname, '../preload/index.js'), - sandbox: false - } - }) - createIPCHandler({ router, windows: [mainWindow] }); - mainWindow.on('ready-to-show', () => { - mainWindow.show() - }) - - mainWindow.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url) - return { action: 'deny' } - }) - - // HMR for renderer base on electron-vite cli. - // Load the remote URL for development or the local html file for production. - if (is.dev && process.env['ELECTRON_RENDERER_URL']) { - mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) - } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html')) - } -} +app.commandLine.appendSwitch("wm-window-animations-disabled") +app.disableHardwareAcceleration() -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. app.whenReady().then(() => { - // Set app user model id for windows - electronApp.setAppUserModelId('com.electron') - - // 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) - }) - - // IPC test - ipcMain.on('ping', () => console.log('pong')) - - createWindow() - app.on('activate', function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) createWindow() + electronApp.setAppUserModelId(config.exeConfig.appId) + nativeTheme.themeSource = "system" + + WindowManager.showMainWindow() + + ipcMain.on('ping', () => { + WindowManager.createWindow("about", { + url: getFileUrl("about.html"), + overideWindowOpts: true, + confrimWindowClose: false, + type: "info", + windowOpts: { + width: 600, + height: 400, + minimizable: false, + darkTheme: true, + modal: true, + title: "关于我", + show: true, + resizable: false, + icon: icon, + webPreferences: { + preload: getPreloadUrl("index"), + devTools: false, + sandbox: false, + nodeIntegration: false, + contextIsolation: true, + }, + }, + }) }) }) - -// Quit when all windows are closed, except on macOS. There, it's common -// for applications and their menu bar to stay active until the user quits -// explicitly with Cmd + Q. -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - } -}) - -// In this file you can include the rest of your app's specific main process -// code. You can also put them in separate files and require them here. diff --git a/src/main/modules/window-manager/index.ts b/src/main/modules/window-manager/index.ts new file mode 100644 index 0000000..c9ace3d --- /dev/null +++ b/src/main/modules/window-manager/index.ts @@ -0,0 +1,359 @@ +import { BrowserWindow, app, dialog } from "electron" +import { cloneDeep, merge } from "lodash-es" +import { defaultConfig, defaultWindowConfig, getWindowsMap, IConfig, Param } from "./windowsMap" +import { optimizer } from "@electron-toolkit/utils" +import { BaseSingleton } from "../../utils/base/base-singleton" +import { trpcBindWindows } from 'trpc/main'; + +declare module "electron" { + interface BrowserWindow { + $$forceClose?: boolean + $$lastChoice?: number + $$opts?: Param + } +} + +class WindowManagerClass extends BaseSingleton { + init(): void { + this.isMainShowReady = new Promise(resolve => { + this.isMainShowResolve = resolve + }) + /** + * 当应用被激活时触发 + */ + 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() + } + }) + } + + globalChioce: number = -1 + #showWin(info: Param) { + if (this.#windows.length >= 6) { + dialog.showErrorBox("错误", "窗口数量超出限制") + return + } + if (!info.name) { + dialog.showErrorBox("错误", "窗口未指定唯一key") + return + } + const index = this.findIndex(info.name) + if (index === -1) { + this.#windows.push(this.#add(info)) + } else { + if (this.#windows[index].isDestroyed()) { + this.#windows[index] = this.#add(info) + } else { + if (info.url && info.loadURLInSameWin) { + this.#windows[index].loadURL(info.url) + } + this.#windows[index].show() + } + } + } + + showMainWindow() { + this.#showWin(this.mainInfo) + this.isMainShowResolve() + } + + private isMainShowResolve + private isMainShowReady + async waitMainShowReady() { + await this.isMainShowReady + } + + createWindow(name: string, opts?: Partial) { + const info = opts as Param + info.name = name + if (!info.ignoreEmptyUrl && !info.url) { + dialog.showErrorBox("错误", name + "窗口未提供url") + return + } + this.#showWin(info as Param) + } + + showWindow(name: string, opts?: Partial) { + let have = false + for (const key in this.#urlMap) { + const info = this.#urlMap[key] + if (new RegExp(key).test(name)) { + opts && merge(info, opts) + info.name = name + if (!info.ignoreEmptyUrl && !info.url) { + dialog.showErrorBox("错误", name + "窗口未提供url") + return + } + this.#showWin(info as Param) + have = true + } + } + if (!have) { + dialog.showErrorBox("错误", name + "窗口未创建成功") + return + } + } + + #urlMap = getWindowsMap() + + getWndows() { + return this.#windows + } + + length() { + return this.#windows.length + } + + public get mainInfo() { + return this.#urlMap["main"] as Param + } + + #windows: BrowserWindow[] = [] + + #defaultConfig: IConfig = defaultConfig + + #add(config: Param) { + const curConfig = cloneDeep(this.#defaultConfig ?? {}) as Omit & { name: string } + 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) + } + } + 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 ?? 0, + cancelId: curConfig.confrimWindowCloseText?.cancelId ?? 0, + 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 集成 + // webPreferences.nodeIntegration = false + + // // 验证正在加载的 URL + // // if (!params.src.startsWith('https://example.com/')) { + // // event.preventDefault() + // // } + // }) + if (curConfig.type === "info") { + // 隐藏菜单 + browserWin.setMenuBarVisibility(false) + } + if (curConfig.url) { + browserWin.loadURL(curConfig.url) + // logger.debug(`当前窗口网址:${curConfig.url}`) + } + if (curConfig.windowOpts?.show === false) { + if (curConfig.url) { + browserWin.once("ready-to-show", () => { + browserWin?.show() + }) + } else { + browserWin?.show() + } + } + trpcBindWindows([browserWin]) + return browserWin + } + + getWindowsLength() { + return this.#windows.length + } + + #onClose(name: string) { + for (let i = this.#windows.length - 1; i >= 0; i--) { + const win = this.#windows[i] + if (name === win.$$opts!.name) { + win.destroy() + this.#windows.splice(i, 1) + } + } + } + + get(name: string) { + return this.#windows.find(v => { + return v.$$opts!.name === name + }) + } + + getFocusWindow() { + const mainWindow = this.getMainWindow() + if (mainWindow?.isFocused()) { + return mainWindow + } + for (let i = 0; i < this.#windows.length; i++) { + const win = this.#windows[i] + if (win.isFocused()) { + return win + } + } + return + } + + getMainWindow() { + return this.#windows.find(v => { + return v.$$opts!.name === this.mainInfo.name + }) + } + + close(name: string | RegExp) { + const indexList = this.findAllIndex(name) + for (let i = indexList.length - 1; i >= 0; i--) { + const index = indexList[i] + const win = this.#windows[index] + win.close() + } + } + + delete(name: string | RegExp) { + const indexList = this.findAllIndex(name) + for (let i = indexList.length - 1; i >= 0; i--) { + const index = indexList[i] + this.#windows.splice(index, 1) + } + } + + findIndex(name: string | RegExp) { + const index = this.#windows.findIndex(v => { + if (typeof name === "string") { + return v.$$opts!.name === name + } else { + return name.test(v.$$opts!.name) + } + }) + return index + } + + findAllIndex(name: string | RegExp) { + const result: number[] = [] + for (let i = 0; i < this.#windows.length; i++) { + const win = this.#windows[i] + if (typeof name === "string" && win.$$opts!.name === name) { + result.push(i) + } else if (typeof name !== "string" && name.test(win.$$opts!.name)) { + result.push(i) + } + } + return result + } + + // show(name: string | RegExp) { + // let indexList = this.findAllIndex(name) + // if (!!indexList.length) { + // for (let i = 0; i < indexList.length; i++) { + // const index = indexList[i]; + // const win = this.#windows[index] + // if (win.isDestroyed()) { + // this.#windows[index] = this.#add(win.$$opts) + // } else { + // win.show() + // } + // } + // } else { + // console.warn("该窗口不存在") + // } + // } +} + +const WindowManager = WindowManagerClass.getInstance() +export { WindowManager } +export default WindowManager \ No newline at end of file diff --git a/src/main/modules/window-manager/windowsMap.ts b/src/main/modules/window-manager/windowsMap.ts new file mode 100644 index 0000000..3283c61 --- /dev/null +++ b/src/main/modules/window-manager/windowsMap.ts @@ -0,0 +1,107 @@ +import Config from "config" +import { BrowserWindowConstructorOptions } from "electron" +import icon from "@res/icon.png?asset" +import { getFileUrl, getPreloadUrl } from "../../utils/file" + +export type Param = Partial & Required> + +export interface IConfig { + name?: string + url?: string + loadURLInSameWin?: boolean + type?: "info" + windowOpts?: BrowserWindowConstructorOptions + overideWindowOpts?: boolean + ignoreEmptyUrl?: boolean + denyWindowOpen?: boolean + confrimWindowClose?: boolean + confrimWindowCloseText?: { + title: string + message: string + buttons: string[] + defaultId: number + cancelId: number + } +} + +export const defaultConfig: IConfig = { + denyWindowOpen: true, +} + +export const defaultWindowConfig = { + height: 600, + useContentSize: true, + width: 800, + show: true, + resizable: true, + minWidth: 900, + minHeight: 600, + frame: true, + transparent: false, + alwaysOnTop: false, + webPreferences: {}, +} + +export function getWindowsMap(): Record { + return { + main: { + name: "main", + url: getFileUrl("index.html"), + confrimWindowClose: true, + confrimWindowCloseText: { + title: Config.exeConfig.name, + defaultId: 0, + cancelId: 0, + message: "确定要关闭吗?", + buttons: ["没事", "直接退出"], + }, + windowOpts: { + show: false, + titleBarStyle: "hidden", + titleBarOverlay: true, + icon: icon, + ...(process.platform === "linux" ? { icon } : {}), + webPreferences: { + webviewTag: false, + preload: getPreloadUrl("index"), + nodeIntegration: false, + contextIsolation: true, + }, + }, + }, + _blank: { + overideWindowOpts: false, + confrimWindowClose: true, + confrimWindowCloseText: { + title: Config.exeConfig.name, + defaultId: 0, + cancelId: 0, + message: "确定要关闭吗?", + buttons: ["没事", "直接退出"], + }, + type: "info", + windowOpts: { + height: 600, + useContentSize: true, + width: 800, + show: true, + resizable: true, + minWidth: 900, + minHeight: 600, + frame: true, + transparent: false, + alwaysOnTop: false, + icon: icon, + title: Config.exeConfig.name, + webPreferences: { + devTools: false, + sandbox: true, + nodeIntegration: false, + contextIsolation: true, + webviewTag: false, + preload: undefined, + }, + }, + }, + } +} diff --git a/src/main/utils/base/base-singleton.ts b/src/main/utils/base/base-singleton.ts new file mode 100644 index 0000000..85c4468 --- /dev/null +++ b/src/main/utils/base/base-singleton.ts @@ -0,0 +1,30 @@ +// 抽象基类,使用泛型来正确推导子类类型 +abstract class BaseSingleton { + private static _instance: any + + public constructor() { + if (this.constructor === BaseSingleton) { + throw new Error("禁止直接实例化 BaseOne 抽象类") + } + + if ((this.constructor as any)._instance) { + throw new Error("构造函数私有化失败,禁止重复 new") + } + + // this.constructor 是子类,所以这里设为 instance + ;(this.constructor as any)._instance = this + } + + abstract init(): void + + public static getInstance(this: new () => T): T { + const clazz = this as any as typeof BaseSingleton + if (!clazz._instance) { + clazz._instance = new this() + clazz._instance.init() + } + return clazz._instance as T + } +} + +export { BaseSingleton } diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts new file mode 100644 index 0000000..3bdc1be --- /dev/null +++ b/src/main/utils/file.ts @@ -0,0 +1,18 @@ +import { is } from "@electron-toolkit/utils" +import { join } from "path" +import { slash } from "./url" + +export function getFileUrl(app: string) { + let winURL = "" + if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { + winURL = process.env["ELECTRON_RENDERER_URL"] + `/${app}#/` + } else { + winURL = join(__dirname, `../renderer/${app}#/`) + } + return slash(winURL) + } + + export function getPreloadUrl(file) { + return join(__dirname, `../preload/${file}.js`) + } + \ No newline at end of file diff --git a/src/main/utils/url.ts b/src/main/utils/url.ts new file mode 100644 index 0000000..c6eff80 --- /dev/null +++ b/src/main/utils/url.ts @@ -0,0 +1,8 @@ +export function slash(path: string) { + const isExtendedLengthPath = path.startsWith("\\\\?\\") + if (isExtendedLengthPath) { + return path + } + return path.replace(/\\/g, "/") + } + \ No newline at end of file diff --git a/src/preload/index.ts b/src/preload/index.ts index 7abcd96..76dee5a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,26 +1 @@ -import { contextBridge } from 'electron' -import { electronAPI } from '@electron-toolkit/preload' -import { exposeElectronTRPC } from 'electron-trpc/main'; - -process.once('loaded', async () => { - exposeElectronTRPC(); -}); -// Custom APIs for renderer -const api = {} - -// Use `contextBridge` APIs to expose Electron APIs to -// renderer only if context isolation is enabled, otherwise -// just add to the DOM global. -if (process.contextIsolated) { - try { - contextBridge.exposeInMainWorld('electron', electronAPI) - contextBridge.exposeInMainWorld('api', api) - } catch (error) { - console.error(error) - } -} else { - // @ts-ignore (define in dts) - window.electron = electronAPI - // @ts-ignore (define in dts) - window.api = api -} +import 'trpc/preload'; diff --git a/src/renderer/about.html b/src/renderer/about.html new file mode 100644 index 0000000..ed6607a --- /dev/null +++ b/src/renderer/about.html @@ -0,0 +1,32 @@ + + + + + + + + + + +
+

关于

+
    +
  • MIT开源
  • +
+
+ + + + \ No newline at end of file diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 22aded3..51ffb02 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -1,18 +1,20 @@ @@ -33,6 +35,7 @@ const rpcHandle = async () => { diff --git a/src/renderer/src/about.ts b/src/renderer/src/about.ts new file mode 100644 index 0000000..afcc586 --- /dev/null +++ b/src/renderer/src/about.ts @@ -0,0 +1,8 @@ + +import { client } from 'trpc/renderer' + +client.subscribeGreeting.subscribe(undefined, { + onData: (data) => { + console.log(data.text); + } +}) \ No newline at end of file diff --git a/src/renderer/src/trpc.ts b/src/renderer/src/trpc.ts deleted file mode 100644 index 4e9b863..0000000 --- a/src/renderer/src/trpc.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createTRPCProxyClient } from '@trpc/client'; -import { ipcLink } from 'electron-trpc/renderer'; -import type { AppRouter } from '../../main/api'; - -export const client = createTRPCProxyClient({ - links: [ipcLink()], -}); diff --git a/tsconfig.node.json b/tsconfig.node.json index db23a68..4d72980 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,8 +1,26 @@ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", - "include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"], + "include": [ + "electron.vite.config.*", + "src/main/**/*", + "src/preload/**/*", + "packages/**/common/**/*", + "packages/**/main/**/*", + "packages/**/preload/**/*" + ], "compilerOptions": { "composite": true, - "types": ["electron-vite/node"] + "types": [ + "electron-vite/node" + ], + "baseUrl": ".", + "paths": { + "main/*": [ + "src/main/*" + ], + "@res": [ + "resources/*" + ] + } } -} +} \ No newline at end of file diff --git a/tsconfig.web.json b/tsconfig.web.json index fcde99b..37aa462 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -5,7 +5,8 @@ "src/renderer/src/**/*", "src/renderer/src/**/*.vue", "src/preload/*.d.ts", - "src/main/api.ts" + "packages/**/common/**/*", + "packages/**/renderer/**/*" ], "compilerOptions": { "composite": true,