Compare commits

...

1 Commits

Author SHA1 Message Date
谢亚昕 427ed9399f 重构更新模块并支持事件作用域机制 3 weeks ago
  1. 2
      config/app_config.json
  2. 78
      packages/base/event/main/index.ts
  3. 3
      packages/helper/updater/common.ts
  4. 16
      packages/helper/updater/events.ts
  5. 23
      packages/helper/updater/main.ts
  6. 6
      packages/helper/updater/main/handler.ts
  7. 0
      packages/helper/updater/renderer.ts
  8. 20
      packages/utils/main/index.ts
  9. 12
      src/common/event/Updater/hook.ts
  10. 4
      src/common/event/Updater/index.ts
  11. 10
      src/common/event/Updater/main/command.ts
  12. 2
      src/main/modules/commands/index.ts
  13. 16
      src/preload/index.ts
  14. 6
      src/renderer/src/pages/index/index.vue
  15. 30
      src/types/global.d.ts

2
config/app_config.json

@ -3,7 +3,7 @@
"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.hoturl": "http://117.21.250.136:9812/ZxqyGateway/biz/file/downApk/%E6%98%93%E4%BC%81%E6%95%B0%E8%BD%AC%E5%B9%B3%E5%8F%B0app-1.2.7.apk",
"update.repo": "wood-desktop",
"update.owner": "npmrun",
"update.allowDowngrade": false,

78
packages/base/event/main/index.ts

@ -1,51 +1,87 @@
// type FireKey = string
// 从完整事件名称中提取原始事件名称(移除作用域前缀)
type ExtractOriginalEventName<T extends string, S extends string> = T extends `${S}:${infer Original}` ? Original : never
// 从完整事件映射中提取原始事件映射
type ExtractBaseEventMaps<T extends Record<string, any>, S extends string> = {
[K in keyof T as ExtractOriginalEventName<K & string, S>]: T[K]
}
type FireFN = (...argu: any[]) => void
class FireEvent<T extends Record<string | symbol, FireFN>> {
#events: Record<keyof T, FireFN[]> = {} as any
// 支持作用域的事件类
class ScopedFireEvent<
T extends Record<string, FireFN>, // 完整的事件映射类型
S extends string, // 作用域类型
B extends Record<string, FireFN> = ExtractBaseEventMaps<T, S>, // 从完整事件映射中提取的基础事件映射
> {
#scope: S
#events: Record<string, FireFN[]> = {}
constructor(scope: S) {
this.#scope = scope
}
// 获取完整的事件名称(添加作用域前缀)
public getFullEventName(eventName: string): string {
if (this.#scope === "") return eventName
return `${this.#scope}:${eventName}`
}
print() {
Object.keys(this.#events).forEach(key => {
console.log(`${key}: ${this.#events[key]}\n`)
})
}
on<S extends keyof T>(name: S, fn: T[S]) {
if (!this.#events[name]) {
this.#events[name] = []
// 订阅事件,支持自动添加作用域前缀
on<K extends keyof B>(name: K, fn: B[K]) {
const fullName = this.getFullEventName(name as string)
if (!this.#events[fullName]) {
this.#events[fullName] = []
}
this.#events[name].push(fn)
this.#events[fullName].push(fn as FireFN)
}
emit<S extends keyof T>(name: S, ...argu: Parameters<T[S]>) {
if (this.#events[name]) {
this.#events[name].forEach(fn => {
// 触发事件,支持自动添加作用域前缀
emit<K extends keyof B>(name: K, ...argu: Parameters<B[K]>) {
const fullName = this.getFullEventName(name as string)
if (this.#events[fullName]) {
this.#events[fullName].forEach(fn => {
fn(...argu)
})
}
}
off<S extends keyof T>(name: S, fn?: T[S]) {
const len = this.#events[name].length
// 取消订阅事件,支持自动添加作用域前缀
off<K extends keyof B>(name: K, fn?: B[K]) {
const fullName = this.getFullEventName(name as string)
const len = this.#events[fullName]?.length || 0
if (!len) {
return
}
if (!fn) {
this.#events[name] = []
this.#events[fullName] = []
} else {
for (let i = len - 1; i >= 0; i--) {
const _fn = this.#events[name][i]
const _fn = this.#events[fullName][i]
if (_fn === fn) {
this.#events[name].splice(i, 1)
this.#events[fullName].splice(i, 1)
}
}
}
}
once<S extends keyof T>(name: S, fn: T[S]) {
const _fn: any = (...argu: any[]) => {
// 一次性订阅事件,支持自动添加作用域前缀
once<K extends keyof B>(name: K, fn: B[K]) {
const _fn: FireFN = (...argu: any[]) => {
fn(...argu)
this.off<S>(name, _fn)
this.off(name, _fn as B[K])
}
this.on(name, _fn)
this.on(name, _fn as B[K])
}
}
export function buildEmitter<T extends Record<string | symbol, FireFN>>() {
return new FireEvent<T>()
// 创建作用域事件发射器的工厂函数 - 仅需一个泛型参数
export function buildEmitter<T extends Record<string | symbol, FireFN>>(scope?: string): ScopedFireEvent<T, typeof scope> {
return new ScopedFireEvent<T, typeof scope>(scope)
}

3
packages/helper/updater/common.ts

@ -1,3 +0,0 @@
export type EventMaps = {
"update-progress": (data: { percent: number; all: number; now: number }) => void
}

16
packages/helper/updater/events.ts

@ -0,0 +1,16 @@
export const EventScope = "updater" as const
// 原始事件映射类型
export type BaseEventMaps = {
"download-progress": (data: { percent: number; all: number; now: number }) => void
error: () => void
"checking-for-update": () => void
"update-available": () => void
"update-not-available": () => void
"update-downloaded": () => void
}
// 将 EventScope 添加到所有事件名称前缀
export type EventMaps = {
[K in keyof BaseEventMaps as `${typeof EventScope}:${K}`]: BaseEventMaps[K]
}

23
packages/helper/updater/main/index.ts → packages/helper/updater/main.ts

@ -2,21 +2,28 @@ import pkg from "electron-updater"
import { app, dialog } from "electron"
import Setting from "setting/main"
import { BaseSingleton } from "base"
import { fetchHotUpdatePackage, flagNeedUpdate } from "./hot"
import { fetchHotUpdatePackage, flagNeedUpdate } from "./main/hot"
import Locales from "locales/main"
import _logger from "logger/main"
import { buildEmitter } from "base/event/main"
import { EventMaps } from "./events"
const logger = _logger.createNamespace("updater")
const { autoUpdater } = pkg
class _Updater extends BaseSingleton {
public events = buildEmitter()
public events = buildEmitter<EventMaps>()
private timer: ReturnType<typeof setInterval> | null = null
// autoReplace = false
async triggerHotUpdate(autoReplace = false) {
const url = Setting.values("update.hoturl")
await fetchHotUpdatePackage(url)
await fetchHotUpdatePackage(url, (p, now, all) => {
this.events.emit("download-progress", {
percent: p,
all,
now,
})
})
flagNeedUpdate()
if (!autoReplace) {
dialog.showMessageBox({
@ -36,27 +43,36 @@ class _Updater extends BaseSingleton {
// 检查更新错误
autoUpdater.on("error", error => {
this.events.emit("error")
logger.debug("Update error:", error)
})
// 检查更新
autoUpdater.on("checking-for-update", () => {
this.events.emit("checking-for-update")
logger.debug("Checking for updates...")
})
// 有可用更新
autoUpdater.on("update-available", info => {
logger.debug("Update available:", info)
this.events.emit("update-available")
this.promptUserToUpdate()
})
// 没有可用更新
autoUpdater.on("update-not-available", info => {
this.events.emit("update-not-available")
logger.debug("Update not available:", info)
})
// 更新下载进度
autoUpdater.on("download-progress", progressObj => {
this.events.emit("download-progress", {
percent: progressObj.percent,
all: progressObj.total,
now: progressObj.transferred,
})
logger.debug(
`Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`,
)
@ -64,6 +80,7 @@ class _Updater extends BaseSingleton {
// 更新下载完成
autoUpdater.on("update-downloaded", info => {
this.events.emit("update-downloaded")
logger.debug("Update downloaded:", info)
this.promptUserToInstall()
})

6
packages/helper/updater/main/handler.ts

@ -1,6 +1,6 @@
import { broadcast } from "utils/main"
import { EventMaps } from "../common"
import { EventMaps, BaseEventMaps, EventScope } from "../events"
export function emit(key: keyof EventMaps, ...args: Parameters<EventMaps[keyof EventMaps]>) {
broadcast(key, ...args)
export function emit(key: keyof BaseEventMaps, ...args: Parameters<EventMaps[keyof EventMaps]>) {
broadcast<EventMaps>(EventScope, key, ...args)
}

0
packages/helper/updater/renderer.ts

20
packages/utils/main/index.ts

@ -3,8 +3,24 @@ import { webContents } from "electron"
import { join } from "node:path"
import { slash } from "utils"
export const broadcast = <T extends Record<string, (...argu: any[]) => void>>(event: keyof T, ...args: Parameters<T[keyof T]>) => {
webContents.getAllWebContents().forEach(browser => browser.send(event as any, ...args))
type FireFN = (...argu: any[]) => void
// 从完整事件名称中提取原始事件名称(移除作用域前缀)
type ExtractOriginalEventName<T extends string, S extends string> = T extends `${S}:${infer Original}` ? Original : never
type ExtractOriginalName<T extends string> = T extends `${infer Original}:${string}` ? Original : never
// 从完整事件映射中提取原始事件映射
type ExtractBaseEventMaps<T extends Record<string, any>, S extends string> = {
[K in keyof T as ExtractOriginalEventName<K & string, S>]: T[K]
}
export const broadcast = <
T extends Record<string, (...argu: any[]) => void>,
B extends Record<string, FireFN> = ExtractBaseEventMaps<T, string>,
>(
scope: ExtractOriginalName<keyof T & string>,
eventName: keyof B,
...args: Parameters<B[keyof B]>
) => {
webContents.getAllWebContents().forEach(browser => browser.send((scope + ":" + (eventName as string)) as any, ...args))
}
export function getFileUrl(app: string) {

12
src/common/event/Updater/hook.ts

@ -1,16 +1,20 @@
import { EventMaps } from "helper/updater/common"
import { EventMaps, EventScope } from "helper/updater/events"
import { defineStore } from "pinia"
import { Updater } from "."
export const useSettingStore = defineStore(
export const useUpdateStore = defineStore(
"Updater",
() => {
getApi<EventMaps>().on("update-progress", (_, data) => {
getApi<EventMaps>().on(EventScope, "download-progress", (_, data) => {
console.log(data)
})
return {}
},
{
persist: false,
},
)
export const triggerHotUpdate = () => {
Updater.getInstance().triggerHotUpdate()
}

4
src/common/event/Updater/index.ts

@ -5,8 +5,8 @@ class Updater extends BaseEvent {
super()
}
test() {
this.api
triggerHotUpdate() {
this.api.call("UpdaterCommand.triggerHotUpdate")
}
}

10
src/common/event/Updater/main/command.ts

@ -1,5 +1,7 @@
import Updater from "helper/updater/main"
import { EventMaps, EventScope } from "helper/updater/events"
import _logger from "logger/main"
import { broadcast } from "utils/main"
const logger = _logger.createNamespace("UpdaterCommand")
@ -9,7 +11,13 @@ export default class UpdaterCommand {
logger.debug("UpdaterCommand init")
}
constructor() {
Updater.events.on("download-progress", ({ percent, all, now }) => {
broadcast<EventMaps>(EventScope, "download-progress", { percent, all, now })
})
}
async triggerHotUpdate() {
Updater.triggerHotUpdate()
await Updater.triggerHotUpdate(false)
}
}

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

@ -89,7 +89,7 @@ export default class Commands extends BaseClass {
})
menu.on("menu-will-close", () => {
this.sendMessage(name, `popup_menu_close:${options.menu_id}`)
this.sendMessage(name, `popupmenu:popup_menu_close:${options.menu_id}`)
// broadcast(`popup_menu_close:${options.menu_id}`)
})
menu.popup(options.popupOptions)

16
src/preload/index.ts

@ -37,19 +37,19 @@ const api = {
if (!command) return
return ipcRenderer.sendSync(command, ...argu)
},
on(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.on(command, cb)
on(scope: string, command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.on(scope + ":" + command, cb)
return () => ipcRenderer.removeListener(command, cb)
},
once(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.once(command, cb)
once(scope: string, command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
ipcRenderer.once(scope + ":" + command, cb)
return () => ipcRenderer.removeListener(command, cb)
},
off(command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
return ipcRenderer.removeListener(command, cb)
off(scope: string, command: string, cb: (event: IpcRendererEvent, ...args: any[]) => void) {
return ipcRenderer.removeListener(scope + ":" + command, cb)
},
offAll(command: string) {
return ipcRenderer.removeAllListeners(command)
offAll(scope: string, command: string) {
return ipcRenderer.removeAllListeners(scope + ":" + command)
},
popupMenu(options: IPopupMenuOption) {
ipcRenderer.send("x_popup_menu", curWebContentName, options)

6
src/renderer/src/pages/index/index.vue

@ -1,7 +1,9 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { triggerHotUpdate } from "common/event/Updater/hook"
</script>
<template>
<div h-full flex flex-col>sad</div>
<div h-full flex flex-col @click="triggerHotUpdate">sad</div>
</template>
<style lang="scss" scoped></style>

30
src/types/global.d.ts

@ -1,15 +1,35 @@
type FireFN = (...argu: any[]) => void
type Api<T extends Record<string | symbol, FireFN>> = {
// 从完整事件名称中提取原始事件名称(移除作用域前缀)
type ExtractOriginalEventName<T extends string, S extends string> = T extends `${S}:${infer Original}` ? Original : never
type ExtractOriginalName<T extends string> = T extends `${infer Original}:${string}` ? Original : never
// 从完整事件映射中提取原始事件映射
type ExtractBaseEventMaps<T extends Record<string, any>, S extends string> = {
[K in keyof T as ExtractOriginalEventName<K & string, S>]: T[K]
}
type Api<T extends Record<string, (...argu: any[]) => void>, B extends Record<string, FireFN> = ExtractBaseEventMaps<T, string>> = {
call: (command: string, ...args: any[]) => any
callLong: (command: string, ...args: any[]) => any
callSync: (command: string, ...args: any[]) => any
send: (command: string, ...argu: any[]) => any
sendSync: (command: string, ...argu: any[]) => any
on: <S extends keyof T>(command: S, cb: (event: IpcRendererEventIpcRendererEvent, ...args: Parameters<T[S]>) => void) => () => void
once: <S extends keyof T>(command: S, cb: (event: IpcRendererEvent, ...args: Parameters<T[S]>) => void) => () => void
off: <S extends keyof T>(command: S, cb: (event: IpcRendererEvent, ...args: Parameters<T[S]>) => void) => void
offAll: <S extends keyof T>(command: S) => void
on: <K extends keyof B>(
scope: ExtractOriginalName<keyof T & string>,
command: K,
cb: (event: IpcRendererEvent, ...args: Parameters<B[K]>) => void,
) => () => void
once: <K extends keyof B>(
scope: ExtractOriginalName<keyof T & string>,
command: K,
cb: (event: IpcRendererEvent, ...args: Parameters<B[K]>) => void,
) => () => void
off: <K extends keyof B>(
scope: ExtractOriginalName<keyof T & string>,
command: K,
cb: (event: IpcRendererEvent, ...args: Parameters<B[K]>) => void,
) => void
offAll: <K extends keyof B>(scope: ExtractOriginalName<keyof T & string>, command: K) => void
popupMenu: (options: IPopupMenuOption) => void
}

Loading…
Cancel
Save