37 changed files with 915 additions and 152 deletions
@ -1,2 +1,3 @@ |
|||||
electron_mirror=https://npmmirror.com/mirrors/electron/ |
electron_mirror=https://npmmirror.com/mirrors/electron/ |
||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ |
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ |
||||
|
link-workspace-packages=true |
||||
|
|||||
@ -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 |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
|
} |
||||
@ -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<Partial<typeof AppConfig>, "common.theme"> & { |
||||
|
language: LanguageType |
||||
|
"common.theme": ThemeType |
||||
|
} |
||||
|
|
||||
|
export default { |
||||
|
appConfig: AppConfig, |
||||
|
exeConfig: ExeConfig, |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
@ -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; |
||||
@ -0,0 +1,3 @@ |
|||||
|
import type { AppRouter } from './app'; |
||||
|
|
||||
|
export { AppRouter }; |
||||
@ -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 []; |
||||
|
}), |
||||
|
}); |
||||
@ -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); |
||||
|
}; |
||||
|
}); |
||||
|
}), |
||||
|
}); |
||||
@ -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; |
||||
@ -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); |
||||
|
}); |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
import { exposeElectronTRPC } from 'electron-trpc/main'; |
||||
|
|
||||
|
process.once('loaded', async () => { |
||||
|
exposeElectronTRPC(); |
||||
|
}); |
||||
@ -1,7 +1,9 @@ |
|||||
import { createTRPCProxyClient } from '@trpc/client'; |
import { createTRPCProxyClient } from '@trpc/client'; |
||||
import { ipcLink } from 'electron-trpc/renderer'; |
import { ipcLink } from 'electron-trpc/renderer'; |
||||
import type { AppRouter } from '../../main/api'; |
import superjson from 'superjson'; |
||||
|
import type { AppRouter } from '../common'; |
||||
|
|
||||
export const client = createTRPCProxyClient<AppRouter>({ |
export const client = createTRPCProxyClient<AppRouter>({ |
||||
links: [ipcLink()], |
links: [ipcLink()], |
||||
|
transformer: superjson, |
||||
}); |
}); |
||||
@ -0,0 +1,2 @@ |
|||||
|
packages: |
||||
|
- "packages/*" |
||||
@ -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; |
|
||||
@ -0,0 +1,9 @@ |
|||||
|
/// <reference types="vite/client" />
|
||||
|
|
||||
|
interface ImportMetaEnv { |
||||
|
// readonly MAIN_VITE_DEBUG: string
|
||||
|
} |
||||
|
|
||||
|
interface ImportMeta { |
||||
|
readonly env: ImportMetaEnv |
||||
|
} |
||||
@ -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<IConfig>) { |
||||
|
const info = opts as Param |
||||
|
info.name = name |
||||
|
if (!info.ignoreEmptyUrl && !info.url) { |
||||
|
dialog.showErrorBox("错误", name + "窗口未提供url") |
||||
|
return |
||||
|
} |
||||
|
this.#showWin(info as Param) |
||||
|
} |
||||
|
|
||||
|
showWindow(name: string, opts?: Partial<IConfig>) { |
||||
|
let have = false |
||||
|
for (const key in this.#urlMap) { |
||||
|
const info = this.#urlMap[key] |
||||
|
if (new RegExp(key).test(name)) { |
||||
|
opts && merge(info, opts) |
||||
|
info.name = name |
||||
|
if (!info.ignoreEmptyUrl && !info.url) { |
||||
|
dialog.showErrorBox("错误", name + "窗口未提供url") |
||||
|
return |
||||
|
} |
||||
|
this.#showWin(info as Param) |
||||
|
have = true |
||||
|
} |
||||
|
} |
||||
|
if (!have) { |
||||
|
dialog.showErrorBox("错误", name + "窗口未创建成功") |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#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<IConfig, "name"> & { 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<WindowManagerClass>() |
||||
|
export { WindowManager } |
||||
|
export default WindowManager |
||||
@ -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<IConfig> & Required<Pick<IConfig, "name">> |
||||
|
|
||||
|
export interface IConfig { |
||||
|
name?: string |
||||
|
url?: string |
||||
|
loadURLInSameWin?: boolean |
||||
|
type?: "info" |
||||
|
windowOpts?: BrowserWindowConstructorOptions |
||||
|
overideWindowOpts?: boolean |
||||
|
ignoreEmptyUrl?: boolean |
||||
|
denyWindowOpen?: boolean |
||||
|
confrimWindowClose?: boolean |
||||
|
confrimWindowCloseText?: { |
||||
|
title: string |
||||
|
message: string |
||||
|
buttons: string[] |
||||
|
defaultId: number |
||||
|
cancelId: number |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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<string, IConfig> { |
||||
|
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, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
@ -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<T extends BaseSingleton>(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 } |
||||
@ -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`) |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,8 @@ |
|||||
|
export function slash(path: string) { |
||||
|
const isExtendedLengthPath = path.startsWith("\\\\?\\") |
||||
|
if (isExtendedLengthPath) { |
||||
|
return path |
||||
|
} |
||||
|
return path.replace(/\\/g, "/") |
||||
|
} |
||||
|
|
||||
@ -1,26 +1 @@ |
|||||
import { contextBridge } from 'electron' |
import 'trpc/preload'; |
||||
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 |
|
||||
} |
|
||||
|
|||||
@ -0,0 +1,32 @@ |
|||||
|
<!doctype html> |
||||
|
<html lang="en"> |
||||
|
|
||||
|
<head> |
||||
|
<meta charset="UTF-8" /> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
|
<style> |
||||
|
html, |
||||
|
body { |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
outline: none; |
||||
|
border: 0; |
||||
|
overflow: hidden; |
||||
|
padding: 10px 20px; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
|
||||
|
<body> |
||||
|
<article> |
||||
|
<h1>关于</h1> |
||||
|
<ul> |
||||
|
<li>MIT开源</li> |
||||
|
</ul> |
||||
|
</article> |
||||
|
<script type="module" src="/src/about.ts"></script> |
||||
|
</body> |
||||
|
|
||||
|
</html> |
||||
@ -0,0 +1,8 @@ |
|||||
|
|
||||
|
import { client } from 'trpc/renderer' |
||||
|
|
||||
|
client.subscribeGreeting.subscribe(undefined, { |
||||
|
onData: (data) => { |
||||
|
console.log(data.text); |
||||
|
} |
||||
|
}) |
||||
@ -1,8 +1,26 @@ |
|||||
{ |
{ |
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json", |
"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": { |
"compilerOptions": { |
||||
"composite": true, |
"composite": true, |
||||
"types": ["electron-vite/node"] |
"types": [ |
||||
|
"electron-vite/node" |
||||
|
], |
||||
|
"baseUrl": ".", |
||||
|
"paths": { |
||||
|
"main/*": [ |
||||
|
"src/main/*" |
||||
|
], |
||||
|
"@res": [ |
||||
|
"resources/*" |
||||
|
] |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
Loading…
Reference in new issue