37 changed files with 915 additions and 152 deletions
@ -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 |
|||
|
|||
@ -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 { 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>({ |
|||
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 { 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'; |
|||
|
|||
@ -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", |
|||
"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/*" |
|||
] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue