diff --git a/config/app_config.json b/config/app_config.json index fa54b1f..c8f5b83 100644 --- a/config/app_config.json +++ b/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, diff --git a/packages/base/event/main/index.ts b/packages/base/event/main/index.ts index 77949d6..0acf06b 100644 --- a/packages/base/event/main/index.ts +++ b/packages/base/event/main/index.ts @@ -1,51 +1,87 @@ -// type FireKey = string +// 从完整事件名称中提取原始事件名称(移除作用域前缀) +type ExtractOriginalEventName = T extends `${S}:${infer Original}` ? Original : never + +// 从完整事件映射中提取原始事件映射 +type ExtractBaseEventMaps, S extends string> = { + [K in keyof T as ExtractOriginalEventName]: T[K] +} + type FireFN = (...argu: any[]) => void -class FireEvent> { - #events: Record = {} as any +// 支持作用域的事件类 +class ScopedFireEvent< + T extends Record, // 完整的事件映射类型 + S extends string, // 作用域类型 + B extends Record = ExtractBaseEventMaps, // 从完整事件映射中提取的基础事件映射 +> { + #scope: S + #events: Record = {} + + 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(name: S, fn: T[S]) { - if (!this.#events[name]) { - this.#events[name] = [] + + // 订阅事件,支持自动添加作用域前缀 + on(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(name: S, ...argu: Parameters) { - if (this.#events[name]) { - this.#events[name].forEach(fn => { + + // 触发事件,支持自动添加作用域前缀 + emit(name: K, ...argu: Parameters) { + const fullName = this.getFullEventName(name as string) + if (this.#events[fullName]) { + this.#events[fullName].forEach(fn => { fn(...argu) }) } } - off(name: S, fn?: T[S]) { - const len = this.#events[name].length + + // 取消订阅事件,支持自动添加作用域前缀 + off(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(name: S, fn: T[S]) { - const _fn: any = (...argu: any[]) => { + + // 一次性订阅事件,支持自动添加作用域前缀 + once(name: K, fn: B[K]) { + const _fn: FireFN = (...argu: any[]) => { fn(...argu) - this.off(name, _fn) + this.off(name, _fn as B[K]) } - this.on(name, _fn) + this.on(name, _fn as B[K]) } } -export function buildEmitter>() { - return new FireEvent() +// 创建作用域事件发射器的工厂函数 - 仅需一个泛型参数 +export function buildEmitter>(scope?: string): ScopedFireEvent { + return new ScopedFireEvent(scope) } diff --git a/packages/helper/updater/common.ts b/packages/helper/updater/common.ts deleted file mode 100644 index b512f5f..0000000 --- a/packages/helper/updater/common.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type EventMaps = { - "update-progress": (data: { percent: number; all: number; now: number }) => void -} diff --git a/packages/helper/updater/events.ts b/packages/helper/updater/events.ts new file mode 100644 index 0000000..70cd7c8 --- /dev/null +++ b/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] +} diff --git a/packages/helper/updater/main/index.ts b/packages/helper/updater/main.ts similarity index 83% rename from packages/helper/updater/main/index.ts rename to packages/helper/updater/main.ts index 82f211c..294a89d 100644 --- a/packages/helper/updater/main/index.ts +++ b/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() private timer: ReturnType | 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() }) diff --git a/packages/helper/updater/main/handler.ts b/packages/helper/updater/main/handler.ts index 8e5c3f9..75374dc 100644 --- a/packages/helper/updater/main/handler.ts +++ b/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) { - broadcast(key, ...args) +export function emit(key: keyof BaseEventMaps, ...args: Parameters) { + broadcast(EventScope, key, ...args) } diff --git a/packages/helper/updater/renderer.ts b/packages/helper/updater/renderer.ts deleted file mode 100644 index e69de29..0000000 diff --git a/packages/utils/main/index.ts b/packages/utils/main/index.ts index 4da7a0c..f9e2084 100644 --- a/packages/utils/main/index.ts +++ b/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 = void>>(event: keyof T, ...args: Parameters) => { - webContents.getAllWebContents().forEach(browser => browser.send(event as any, ...args)) +type FireFN = (...argu: any[]) => void +// 从完整事件名称中提取原始事件名称(移除作用域前缀) +type ExtractOriginalEventName = T extends `${S}:${infer Original}` ? Original : never +type ExtractOriginalName = T extends `${infer Original}:${string}` ? Original : never +// 从完整事件映射中提取原始事件映射 +type ExtractBaseEventMaps, S extends string> = { + [K in keyof T as ExtractOriginalEventName]: T[K] +} + +export const broadcast = < + T extends Record void>, + B extends Record = ExtractBaseEventMaps, +>( + scope: ExtractOriginalName, + eventName: keyof B, + ...args: Parameters +) => { + webContents.getAllWebContents().forEach(browser => browser.send((scope + ":" + (eventName as string)) as any, ...args)) } export function getFileUrl(app: string) { diff --git a/src/common/event/Updater/hook.ts b/src/common/event/Updater/hook.ts index 0cfc34a..2733b9e 100644 --- a/src/common/event/Updater/hook.ts +++ b/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().on("update-progress", (_, data) => { + getApi().on(EventScope, "download-progress", (_, data) => { console.log(data) }) - return {} }, { persist: false, }, ) + +export const triggerHotUpdate = () => { + Updater.getInstance().triggerHotUpdate() +} diff --git a/src/common/event/Updater/index.ts b/src/common/event/Updater/index.ts index 2b23ee4..06f557c 100644 --- a/src/common/event/Updater/index.ts +++ b/src/common/event/Updater/index.ts @@ -5,8 +5,8 @@ class Updater extends BaseEvent { super() } - test() { - this.api + triggerHotUpdate() { + this.api.call("UpdaterCommand.triggerHotUpdate") } } diff --git a/src/common/event/Updater/main/command.ts b/src/common/event/Updater/main/command.ts index 8ba7617..efa28c6 100644 --- a/src/common/event/Updater/main/command.ts +++ b/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(EventScope, "download-progress", { percent, all, now }) + }) + } + async triggerHotUpdate() { - Updater.triggerHotUpdate() + await Updater.triggerHotUpdate(false) } } diff --git a/src/main/modules/commands/index.ts b/src/main/modules/commands/index.ts index c7f97df..8bb8daf 100644 --- a/src/main/modules/commands/index.ts +++ b/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) diff --git a/src/preload/index.ts b/src/preload/index.ts index 58b2883..045ac47 100644 --- a/src/preload/index.ts +++ b/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) diff --git a/src/renderer/src/pages/index/index.vue b/src/renderer/src/pages/index/index.vue index a55ca47..150ccd5 100644 --- a/src/renderer/src/pages/index/index.vue +++ b/src/renderer/src/pages/index/index.vue @@ -1,7 +1,9 @@ - + diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 49cab64..af4b4ef 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,15 +1,35 @@ type FireFN = (...argu: any[]) => void -type Api> = { +// 从完整事件名称中提取原始事件名称(移除作用域前缀) +type ExtractOriginalEventName = T extends `${S}:${infer Original}` ? Original : never +type ExtractOriginalName = T extends `${infer Original}:${string}` ? Original : never +// 从完整事件映射中提取原始事件映射 +type ExtractBaseEventMaps, S extends string> = { + [K in keyof T as ExtractOriginalEventName]: T[K] +} + +type Api void>, B extends Record = ExtractBaseEventMaps> = { 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: (command: S, cb: (event: IpcRendererEventIpcRendererEvent, ...args: Parameters) => void) => () => void - once: (command: S, cb: (event: IpcRendererEvent, ...args: Parameters) => void) => () => void - off: (command: S, cb: (event: IpcRendererEvent, ...args: Parameters) => void) => void - offAll: (command: S) => void + on: ( + scope: ExtractOriginalName, + command: K, + cb: (event: IpcRendererEvent, ...args: Parameters) => void, + ) => () => void + once: ( + scope: ExtractOriginalName, + command: K, + cb: (event: IpcRendererEvent, ...args: Parameters) => void, + ) => () => void + off: ( + scope: ExtractOriginalName, + command: K, + cb: (event: IpcRendererEvent, ...args: Parameters) => void, + ) => void + offAll: (scope: ExtractOriginalName, command: K) => void popupMenu: (options: IPopupMenuOption) => void }