Files
ClassHelper/packages/desktop-client/src/preload/index.ts
T

109 lines
5.7 KiB
TypeScript
Raw Normal View History

import type {
ReleaseProgressDto as ContractReleaseProgressDto,
CoursewareTransfer,
} from '@classhelper/shared';
import { contextBridge, ipcRenderer } from 'electron';
import { IPC_CHANNELS, UPDATE_AVAILABLE_CHANNEL } from '../main/channels.js';
import type { ClientAgentStatus } from '../main/client-agent.js';
import type {
DesktopAppInfo,
DesktopBridge,
DesktopStoredConfig,
IslandClassStatePayload,
IslandPushContext,
} from '../types/desktop.js';
import type { IslandNotification, UpdateInfo } from '@classhelper/shared';
/**
* 预加载脚本:在开启了 contextIsolation + sandbox 的前提下,
* 只向渲染进程暴露一组最小的、显式声明的能力(不暴露 ipcRenderer 本体)。
*
* 注意:contextBridge 在参数跨越"主世界 → 隔离世界"时就做结构化克隆,
* 因此**桥接层无法替调用方兜底**:传 Vue 响应式对象(Proxy)会在进入这里的函数体
* 之前就抛 `An object could not be cloned.`。调用方必须传纯数据,见 desktop.d.ts。
*/
const bridge: DesktopBridge = {
downloadCourseware: (id) => ipcRenderer.invoke('courseware:download', id),
revealCourseware: (id) => ipcRenderer.invoke('courseware:reveal', id),
listLocalCourseware: () => ipcRenderer.invoke('courseware:local-list'),
syncCourseware: () => ipcRenderer.invoke('courseware:sync'),
getCoursewareTransfer: () => ipcRenderer.invoke('courseware:get-progress'),
onCoursewareTransfer: (handler) => {
const listener = (_event: unknown, value: CoursewareTransfer | null) => handler(value);
ipcRenderer.on('courseware:progress', listener);
return () => ipcRenderer.removeListener('courseware:progress', listener);
},
islandEditLayout: () => ipcRenderer.invoke('island:edit-layout'),
onIslandEditorOpen: (handler) => {
const listener = () => handler();
ipcRenderer.on('island:open-editor', listener);
return () => ipcRenderer.removeListener('island:open-editor', listener);
},
smokeTest: process.env.ELECTRON_SMOKE_TEST === '1',
// 冒烟凭据:只在冒烟模式下透传,供渲染进程的冒烟钩子构造测试数据用。
// **不走代码字面量**——本文件与渲染产物都会进 app.asar,写死账号等于随安装包把教师口令发出去。
smokeCredentials:
process.env.ELECTRON_SMOKE_TEST === '1'
? {
username: process.env.ELECTRON_SMOKE_USER ?? '',
password: process.env.ELECTRON_SMOKE_PASSWORD ?? '',
classCode: process.env.ELECTRON_SMOKE_CLASS_CODE ?? '',
classPassword: process.env.ELECTRON_SMOKE_CLASS_PASSWORD ?? '',
}
: undefined,
getUpdateProgress: () => ipcRenderer.invoke('client:get-update-progress'),
onUpdateProgress: (handler: (progress: ContractReleaseProgressDto) => void) => {
const listener = (_event: unknown, value: ContractReleaseProgressDto) => handler(value);
ipcRenderer.on('client:update-progress', listener);
return () => ipcRenderer.removeListener('client:update-progress', listener);
},
getConfig: (): Promise<DesktopStoredConfig> => ipcRenderer.invoke(IPC_CHANNELS.getConfig),
saveConfig: (patch: Partial<DesktopStoredConfig>): Promise<DesktopStoredConfig> =>
ipcRenderer.invoke(IPC_CHANNELS.saveConfig, patch),
clearConfig: (): Promise<DesktopStoredConfig> => ipcRenderer.invoke(IPC_CHANNELS.clearConfig),
getAppInfo: (): Promise<DesktopAppInfo> => ipcRenderer.invoke(IPC_CHANNELS.appInfo),
openExternal: (url: string): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openExternal, url),
/* 更新检查(主进程直连 GitHub,见 main/update.ts) */
checkForUpdates: (force = false): Promise<UpdateInfo> =>
ipcRenderer.invoke(IPC_CHANNELS.updateCheck, force),
ignoreUpdateVersion: (version: string): Promise<void> =>
ipcRenderer.invoke(IPC_CHANNELS.updateIgnore, version),
onUpdateAvailable: (handler: (info: UpdateInfo) => void): void => {
ipcRenderer.on(UPDATE_AVAILABLE_CHANNEL, (_event, info: UpdateInfo) => handler(info));
},
islandPush: (payload: { notification: IslandNotification; context?: IslandPushContext }): void => {
ipcRenderer.send('island:push', payload);
},
islandRemoveNotification: (id: string): void => {
ipcRenderer.send('island:remove-notification', id);
},
islandSetClassState: (payload: IslandClassStatePayload): void => {
ipcRenderer.send('island:class-state', payload);
},
islandGetState: () => ipcRenderer.invoke('island:get-state'),
islandSetAppearance: (appearance): void => {
ipcRenderer.send('island:set-appearance', appearance);
},
islandGetAppearance: () => ipcRenderer.invoke('island:get-appearance'),
/** 灵动岛点了"标为已读":主进程转交渲染进程同步通知中心 */
onIslandMarkRead: (handler: (id: string) => void): void => {
ipcRenderer.on('island:mark-read', (_event, id: string) => handler(id));
},
/** 灵动岛点了"标为已读"(多条通知的整批):一次同步这一批 */
onIslandMarkAllRead: (handler: (ids: string[]) => void): void => {
ipcRenderer.on('island:mark-all-read', (_event, ids: string[]) => handler(ids ?? []));
},
/* 开机自启 / 日志 / 服务端联动 */
getAutoLaunch: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.getAutoLaunch),
setAutoLaunch: (enabled: boolean): Promise<boolean> =>
ipcRenderer.invoke(IPC_CHANNELS.setAutoLaunch, enabled === true),
openLogDir: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openLogDir),
openDevTools: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openDevTools),
getClientAgentStatus: (): Promise<ClientAgentStatus> => ipcRenderer.invoke(IPC_CHANNELS.clientStatus),
beatClientNow: (): Promise<ClientAgentStatus> => ipcRenderer.invoke(IPC_CHANNELS.clientBeatNow),
};
contextBridge.exposeInMainWorld('desktop', bridge);