返回 oh-my-ppt
master-handlers.ts
根目录 / src / main / session / master-handlers.ts
1 import { ipcMain } from 'electron'
2 import {
3 MASTER_BACKGROUND_MODES,
4 MASTER_BACKGROUND_STYLES,
5 MASTER_FONT_PRESETS,
6 MASTER_GRADIENT_TYPES,
7 MAX_MASTER_BODY_FONT_SIZE,
8 MAX_MASTER_TITLE_FONT_SIZE,
9 MAX_MASTER_GRADIENT_STOPS,
10 MIN_MASTER_BODY_FONT_SIZE,
11 MIN_MASTER_TITLE_FONT_SIZE,
12 MIN_MASTER_GRADIENT_STOPS,
13 normalizeMasterConfig,
14 type SessionMasterConfig
15 } from '@shared/master'
16 import { isValidSessionLayoutLibrary, type SessionLayoutLibrary } from '@shared/layout-master'
17 import type { IpcContext } from '../ipc/context'
18 import {
19 getSessionMasterStatus,
20 getSessionLayoutLibraryStatus,
21 saveSessionLayoutLibrary,
22 saveSessionMaster,
23 setSessionMasterPageOverride
24 } from './master-mutation-service'
25
26 const getPayload = (value: unknown): Record<string, unknown> =>
27 value && typeof value === 'object' && !Array.isArray(value)
28 ? (value as Record<string, unknown>)
29 : {}
30
31 const isRecord = (value: unknown): value is Record<string, unknown> =>
32 Boolean(value) && typeof value === 'object' && !Array.isArray(value)
33
34 const isValidGradientStop = (value: unknown): boolean =>
35 isRecord(value) &&
36 typeof value.color === 'string' &&
37 /^#[0-9a-fA-F]{6}$/.test(value.color) &&
38 typeof value.position === 'number' &&
39 Number.isFinite(value.position) &&
40 value.position >= 0 &&
41 value.position <= 100
42
43 const isValidGradient = (value: unknown): boolean => {
44 if (!isRecord(value)) return false
45 if (!MASTER_GRADIENT_TYPES.includes(value.type as (typeof MASTER_GRADIENT_TYPES)[number]))
46 return false
47 if (
48 typeof value.angle !== 'number' ||
49 !Number.isFinite(value.angle) ||
50 value.angle < 0 ||
51 value.angle > 359
52 ) {
53 return false
54 }
55 if (!Array.isArray(value.stops)) return false
56 return (
57 value.stops.length >= MIN_MASTER_GRADIENT_STOPS &&
58 value.stops.length <= MAX_MASTER_GRADIENT_STOPS &&
59 value.stops.every(isValidGradientStop)
60 )
61 }
62
63 const requireSessionId = (value: unknown): string => {
64 const sessionId = typeof value === 'string' ? value.trim() : ''
65 if (!sessionId) throw new Error('缺少 sessionId')
66 return sessionId
67 }
68
69 const isValidBackgroundImage = (value: unknown): boolean => {
70 if (value === undefined || value === null) return true
71 if (typeof value !== 'string') return false
72 const imagePath = value.trim()
73 const fileName = imagePath.slice('./images/'.length)
74 return (
75 imagePath.startsWith('./images/') &&
76 fileName.length > 0 &&
77 fileName !== '.' &&
78 fileName !== '..' &&
79 !fileName.includes('/') &&
80 !fileName.includes('\\') &&
81 !fileName.includes('\0')
82 )
83 }
84
85 const isValidMasterElementText = (value: unknown, maxLength: number): boolean =>
86 value === undefined ||
87 (typeof value === 'string' && value.replace(/\s+/g, ' ').trim().length <= maxLength)
88
89 const isValidMasterElementPosition = (value: unknown): boolean =>
90 value === undefined ||
91 (isRecord(value) &&
92 typeof value.x === 'number' &&
93 Number.isFinite(value.x) &&
94 value.x >= 0 &&
95 value.x <= 100 &&
96 typeof value.y === 'number' &&
97 Number.isFinite(value.y) &&
98 value.y >= 0 &&
99 value.y <= 100)
100
101 const isValidMasterElementSize = (value: unknown): boolean =>
102 value === undefined ||
103 (isRecord(value) &&
104 typeof value.width === 'number' &&
105 Number.isFinite(value.width) &&
106 value.width >= 1 &&
107 value.width <= 100 &&
108 typeof value.height === 'number' &&
109 Number.isFinite(value.height) &&
110 value.height >= 1 &&
111 value.height <= 100)
112
113 const isValidMasterElementVisibility = (value: unknown): boolean =>
114 value === undefined || typeof value === 'boolean'
115
116 const isValidMasterElementFontSize = (value: unknown): boolean =>
117 value === undefined ||
118 (typeof value === 'number' &&
119 Number.isFinite(value) &&
120 Number.isInteger(value) &&
121 value >= 8 &&
122 value <= 160)
123
124 const isValidMasterElementRotation = (value: unknown): boolean =>
125 value === undefined ||
126 (typeof value === 'number' &&
127 Number.isFinite(value) &&
128 Number.isInteger(value) &&
129 value >= -180 &&
130 value <= 180)
131
132 const isValidMasterElementColor = (value: unknown): boolean =>
133 value === undefined || (typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value))
134
135 const isValidMasterElements = (value: unknown): boolean => {
136 if (value === undefined) return true
137 if (!isRecord(value)) return false
138 return (
139 isValidBackgroundImage(value.logoImage) &&
140 isValidMasterElementText(value.footerText, 180) &&
141 isValidMasterElementText(value.watermarkText, 80) &&
142 isValidMasterElementVisibility(value.showLogo) &&
143 isValidMasterElementVisibility(value.showFooter) &&
144 isValidMasterElementVisibility(value.showPageNumber) &&
145 isValidMasterElementVisibility(value.showWatermark) &&
146 isValidMasterElementFontSize(value.footerFontSize) &&
147 isValidMasterElementFontSize(value.pageNumberFontSize) &&
148 isValidMasterElementColor(value.footerColor) &&
149 isValidMasterElementColor(value.pageNumberColor) &&
150 isValidMasterElementRotation(value.watermarkRotation) &&
151 isValidMasterElementVisibility(value.watermarkSizeAuto) &&
152 isValidMasterElementPosition(value.logoPosition) &&
153 isValidMasterElementPosition(value.footerPosition) &&
154 isValidMasterElementPosition(value.pageNumberPosition) &&
155 isValidMasterElementPosition(value.watermarkPosition) &&
156 isValidMasterElementSize(value.logoSize) &&
157 isValidMasterElementSize(value.footerSize) &&
158 isValidMasterElementSize(value.pageNumberSize) &&
159 isValidMasterElementSize(value.watermarkSize)
160 )
161 }
162
163 const requireMasterConfig = (value: unknown): SessionMasterConfig => {
164 if (!value || typeof value !== 'object' || Array.isArray(value)) {
165 throw new Error('母版配置无效')
166 }
167 const input = value as Record<string, unknown>
168 const isValidFontFamily = (font: unknown): boolean =>
169 font === undefined ||
170 font === null ||
171 (typeof font === 'string' && font.trim().length > 0 && font.trim().length <= 120)
172 const isValidFontSize = (fontSize: unknown, min: number, max: number): boolean =>
173 fontSize === undefined ||
174 fontSize === null ||
175 (typeof fontSize === 'number' &&
176 Number.isInteger(fontSize) &&
177 Number.isFinite(fontSize) &&
178 fontSize >= min &&
179 fontSize <= max)
180 if (
181 typeof input.backgroundColor !== 'string' ||
182 !/^#[0-9a-fA-F]{6}$/.test(input.backgroundColor.trim()) ||
183 typeof input.backgroundMode !== 'string' ||
184 !MASTER_BACKGROUND_MODES.includes(
185 input.backgroundMode as SessionMasterConfig['backgroundMode']
186 ) ||
187 typeof input.backgroundStyle !== 'string' ||
188 !MASTER_BACKGROUND_STYLES.includes(
189 input.backgroundStyle as SessionMasterConfig['backgroundStyle']
190 ) ||
191 !isValidGradient(input.backgroundGradient) ||
192 !isValidBackgroundImage(input.backgroundImage) ||
193 (input.backgroundStyle === 'image' && typeof input.backgroundImage !== 'string') ||
194 typeof input.titleFontPreset !== 'string' ||
195 !MASTER_FONT_PRESETS.includes(
196 input.titleFontPreset as SessionMasterConfig['titleFontPreset']
197 ) ||
198 typeof input.bodyFontPreset !== 'string' ||
199 !MASTER_FONT_PRESETS.includes(input.bodyFontPreset as SessionMasterConfig['bodyFontPreset']) ||
200 !isValidFontFamily(input.titleFontFamily) ||
201 !isValidFontFamily(input.bodyFontFamily) ||
202 !isValidFontSize(input.titleFontSize, MIN_MASTER_TITLE_FONT_SIZE, MAX_MASTER_TITLE_FONT_SIZE) ||
203 !isValidFontSize(input.bodyFontSize, MIN_MASTER_BODY_FONT_SIZE, MAX_MASTER_BODY_FONT_SIZE) ||
204 !isValidMasterElements(input.elements)
205 ) {
206 throw new Error('母版配置无效')
207 }
208 return normalizeMasterConfig(input)
209 }
210
211 const requirePageOverride = (value: unknown): { pageId: string; disabled: boolean } => {
212 const input = getPayload(value)
213 const pageId = typeof input.pageId === 'string' ? input.pageId.trim() : ''
214 if (!pageId || typeof input.disabled !== 'boolean') throw new Error('页面母版设置无效')
215 return { pageId, disabled: input.disabled }
216 }
217
218 const requireSessionLayoutLibrary = (value: unknown): SessionLayoutLibrary => {
219 if (!isValidSessionLayoutLibrary(value)) throw new Error('版式母版配置无效')
220 return value
221 }
222
223 export function registerMasterHandlers(ctx: IpcContext): void {
224 ipcMain.handle('session:getMaster', async (_event, payload: unknown) => {
225 const { sessionId } = getPayload(payload)
226 return getSessionMasterStatus(ctx, requireSessionId(sessionId))
227 })
228
229 ipcMain.handle('session:saveMaster', async (_event, payload: unknown) => {
230 const { sessionId, config } = getPayload(payload)
231 return saveSessionMaster(ctx, requireSessionId(sessionId), requireMasterConfig(config))
232 })
233
234 ipcMain.handle('session:setMasterPageOverride', async (_event, payload: unknown) => {
235 const { sessionId } = getPayload(payload)
236 const { pageId, disabled } = requirePageOverride(payload)
237 return setSessionMasterPageOverride(ctx, requireSessionId(sessionId), pageId, disabled)
238 })
239
240 ipcMain.handle('session:getLayoutLibrary', async (_event, payload: unknown) => {
241 const { sessionId } = getPayload(payload)
242 return getSessionLayoutLibraryStatus(ctx, requireSessionId(sessionId))
243 })
244
245 ipcMain.handle('session:saveLayoutLibrary', async (_event, payload: unknown) => {
246 const { sessionId, library } = getPayload(payload)
247 return saveSessionLayoutLibrary(
248 ctx,
249 requireSessionId(sessionId),
250 requireSessionLayoutLibrary(library)
251 )
252 })
253 }
254
254 lines TYPESCRIPT