beauticode-dsh 1.0.12 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/ui-host.mjs +144 -79
package/package.json
CHANGED
package/ui-host.mjs
CHANGED
|
@@ -15,6 +15,7 @@ const MAX_IMAGE_BYTES = 18 * 1024 * 1024;
|
|
|
15
15
|
const MAX_VIDEO_BYTES = 800 * 1024 * 1024;
|
|
16
16
|
const SELECTION_TTL_MS = 5 * 60 * 1000;
|
|
17
17
|
const MAX_PENDING_SELECTIONS = 16;
|
|
18
|
+
const PICKER_TIMEOUT_MS = 5 * 60 * 1000;
|
|
18
19
|
const NATIVE_PICKER_UNAVAILABLE = "native_picker_unavailable";
|
|
19
20
|
const NATIVE_PICKER_REQUIRED = "native_picker_required";
|
|
20
21
|
const PICKER_REQUEST_ABORTED = "picker_request_aborted";
|
|
@@ -107,103 +108,150 @@ function suggestedThemeName(fileName, fallback) {
|
|
|
107
108
|
return stem || fallback;
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
export function buildWindowsPickerScript(
|
|
112
|
+
kind,
|
|
113
|
+
{ parentPid = process.pid, timeoutMs = PICKER_TIMEOUT_MS } = {},
|
|
114
|
+
) {
|
|
114
115
|
const filter =
|
|
115
116
|
kind === "video"
|
|
116
117
|
? "MP4 Video (*.mp4)|*.mp4"
|
|
117
118
|
: "Image Files (*.jpg;*.jpeg;*.png;*.webp;*.avif)|*.jpg;*.jpeg;*.png;*.webp;*.avif";
|
|
118
|
-
const
|
|
119
|
+
const safeParentPid = Number.isSafeInteger(parentPid) && parentPid > 0
|
|
120
|
+
? parentPid
|
|
121
|
+
: process.pid;
|
|
122
|
+
const safeTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
123
|
+
? Math.ceil(timeoutMs)
|
|
124
|
+
: PICKER_TIMEOUT_MS;
|
|
125
|
+
return [
|
|
119
126
|
"$ErrorActionPreference = 'Stop'",
|
|
120
127
|
"$OutputEncoding = New-Object System.Text.UTF8Encoding($false)",
|
|
121
128
|
"[Console]::OutputEncoding = $OutputEncoding",
|
|
122
129
|
"Add-Type -AssemblyName System.Windows.Forms",
|
|
130
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
123
131
|
"[System.Windows.Forms.Application]::EnableVisualStyles()",
|
|
132
|
+
"$owner = New-Object System.Windows.Forms.Form",
|
|
133
|
+
"$owner.Text = 'beautiCode 文件选择器'",
|
|
134
|
+
"$owner.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow",
|
|
135
|
+
"$owner.ShowInTaskbar = $false",
|
|
136
|
+
"$owner.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen",
|
|
137
|
+
"$owner.Size = [System.Drawing.Size]::new(1, 1)",
|
|
138
|
+
"$owner.Opacity = 0.01",
|
|
139
|
+
"$owner.TopMost = $true",
|
|
124
140
|
"$dialog = New-Object System.Windows.Forms.OpenFileDialog",
|
|
125
141
|
`$dialog.Filter = '${filter}'`,
|
|
126
142
|
"$dialog.Multiselect = $false",
|
|
127
143
|
"$dialog.CheckFileExists = $true",
|
|
144
|
+
"$dialog.RestoreDirectory = $true",
|
|
128
145
|
"$dialog.Title = '选择 beautiCode 背景文件'",
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
"$
|
|
146
|
+
`$dshPid = ${safeParentPid}`,
|
|
147
|
+
`$deadlineUtc = [DateTime]::UtcNow.AddMilliseconds(${safeTimeoutMs})`,
|
|
148
|
+
"$watchdog = New-Object System.Windows.Forms.Timer",
|
|
149
|
+
"$watchdog.Interval = 500",
|
|
150
|
+
"$watchdog.Add_Tick({ if ([DateTime]::UtcNow -ge $deadlineUtc -or -not (Get-Process -Id $dshPid -ErrorAction SilentlyContinue)) { $watchdog.Stop(); [Environment]::Exit(0) } })",
|
|
151
|
+
"try { [void]$owner.Show(); [void]$owner.Hide(); [void]$owner.Show(); [void]$owner.Activate(); [void]$owner.BringToFront(); [System.Windows.Forms.Application]::DoEvents(); if (-not $owner.IsHandleCreated) { throw 'Picker owner window was not created.' }; $watchdog.Start(); $result = $dialog.ShowDialog($owner); if ($result -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::WriteLine($dialog.FileName) } } finally { $watchdog.Stop(); $watchdog.Dispose(); $dialog.Dispose(); $owner.Close(); $owner.Dispose() }",
|
|
132
152
|
].join("; ");
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
reject(new Error("文件选择器超时。"));
|
|
166
|
-
}, 5 * 60 * 1000);
|
|
167
|
-
if (signal?.aborted) onAbort();
|
|
168
|
-
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
169
|
-
child.stdout.setEncoding("utf8");
|
|
170
|
-
child.stderr.setEncoding("utf8");
|
|
171
|
-
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
172
|
-
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
173
|
-
child.once("error", (error) => {
|
|
174
|
-
clearTimeout(timer);
|
|
175
|
-
signal?.removeEventListener("abort", onAbort);
|
|
176
|
-
if (aborted) return;
|
|
177
|
-
reject(
|
|
178
|
-
error?.code === "ENOENT"
|
|
179
|
-
? pickerUnavailable("找不到 Windows PowerShell,无法打开原生文件选择器。")
|
|
180
|
-
: error,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createWindowsMediaPicker({
|
|
156
|
+
platform = process.platform,
|
|
157
|
+
spawnProcess = spawn,
|
|
158
|
+
parentProcess = process,
|
|
159
|
+
parentPid = process.pid,
|
|
160
|
+
timeoutMs = PICKER_TIMEOUT_MS,
|
|
161
|
+
} = {}) {
|
|
162
|
+
return function pickWindowsMedia(kind, { signal } = {}) {
|
|
163
|
+
if (platform !== "win32") {
|
|
164
|
+
throw pickerUnavailable("当前系统不支持 Windows 原生文件选择器。");
|
|
165
|
+
}
|
|
166
|
+
const script = buildWindowsPickerScript(kind, { parentPid, timeoutMs });
|
|
167
|
+
const encodedScript = Buffer.from(script, "utf16le").toString("base64");
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const child = spawnProcess(
|
|
170
|
+
"powershell.exe",
|
|
171
|
+
[
|
|
172
|
+
"-NoLogo",
|
|
173
|
+
"-NoProfile",
|
|
174
|
+
"-ExecutionPolicy",
|
|
175
|
+
"Bypass",
|
|
176
|
+
"-STA",
|
|
177
|
+
"-EncodedCommand",
|
|
178
|
+
encodedScript,
|
|
179
|
+
],
|
|
180
|
+
{
|
|
181
|
+
// Hide PowerShell's console while leaving the owned WinForms dialog visible.
|
|
182
|
+
windowsHide: true,
|
|
183
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
184
|
+
},
|
|
181
185
|
);
|
|
186
|
+
let stdout = "";
|
|
187
|
+
let stderr = "";
|
|
188
|
+
let settled = false;
|
|
189
|
+
const cleanup = () => {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
signal?.removeEventListener("abort", onAbort);
|
|
192
|
+
parentProcess.removeListener("exit", onParentExit);
|
|
193
|
+
};
|
|
194
|
+
const settle = (callback, value, { terminate = false } = {}) => {
|
|
195
|
+
if (settled) return;
|
|
196
|
+
settled = true;
|
|
197
|
+
cleanup();
|
|
198
|
+
if (terminate && !child.killed) child.kill("SIGKILL");
|
|
199
|
+
callback(value);
|
|
200
|
+
};
|
|
201
|
+
const onAbort = () => {
|
|
202
|
+
const error = new Error("文件选择请求已取消。");
|
|
203
|
+
error.code = PICKER_REQUEST_ABORTED;
|
|
204
|
+
settle(reject, error, { terminate: true });
|
|
205
|
+
};
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
settle(reject, new Error("文件选择器超时。"), { terminate: true });
|
|
208
|
+
}, timeoutMs);
|
|
209
|
+
const onParentExit = () => {
|
|
210
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
211
|
+
};
|
|
212
|
+
parentProcess.once("exit", onParentExit);
|
|
213
|
+
if (signal?.aborted) onAbort();
|
|
214
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
215
|
+
child.stdout.setEncoding("utf8");
|
|
216
|
+
child.stderr.setEncoding("utf8");
|
|
217
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
218
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
219
|
+
child.once("error", (error) => {
|
|
220
|
+
settle(
|
|
221
|
+
reject,
|
|
222
|
+
error?.code === "ENOENT"
|
|
223
|
+
? pickerUnavailable("找不到 Windows PowerShell,无法打开原生文件选择器。")
|
|
224
|
+
: error,
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
child.once("close", (code) => {
|
|
228
|
+
if (settled) return;
|
|
229
|
+
if (code !== 0) {
|
|
230
|
+
const message = stderr.trim() || `文件选择器退出异常(${code})。`;
|
|
231
|
+
settle(
|
|
232
|
+
reject,
|
|
233
|
+
isPickerDependencyError(message) ? pickerUnavailable(message) : new Error(message),
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const selected = stdout.trim();
|
|
238
|
+
if (!selected) {
|
|
239
|
+
settle(resolve, { ok: true, cancelled: true });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const name = path.basename(selected.replaceAll("\\", "/"));
|
|
243
|
+
const parsed = parseImportFilename(name);
|
|
244
|
+
if (!parsed.ok || parsed.kind !== kind) {
|
|
245
|
+
settle(reject, new Error(parsed.ok ? "选择的文件类型不匹配。" : parsed.error));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
settle(resolve, { ok: true, kind, path: path.resolve(selected), name });
|
|
249
|
+
});
|
|
182
250
|
});
|
|
183
|
-
|
|
184
|
-
clearTimeout(timer);
|
|
185
|
-
signal?.removeEventListener("abort", onAbort);
|
|
186
|
-
if (aborted) return;
|
|
187
|
-
if (code !== 0) {
|
|
188
|
-
const message = stderr.trim() || `文件选择器退出异常(${code})。`;
|
|
189
|
-
reject(isPickerDependencyError(message) ? pickerUnavailable(message) : new Error(message));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const selected = stdout.trim();
|
|
193
|
-
if (!selected) {
|
|
194
|
-
resolve({ ok: true, cancelled: true });
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const name = path.basename(selected.replaceAll("\\", "/"));
|
|
198
|
-
const parsed = parseImportFilename(name);
|
|
199
|
-
if (!parsed.ok || parsed.kind !== kind) {
|
|
200
|
-
reject(new Error(parsed.ok ? "选择的文件类型不匹配。" : parsed.error));
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
resolve({ ok: true, kind, path: path.resolve(selected), name });
|
|
204
|
-
});
|
|
205
|
-
});
|
|
251
|
+
};
|
|
206
252
|
}
|
|
253
|
+
|
|
254
|
+
const pickWindowsMedia = createWindowsMediaPicker();
|
|
207
255
|
|
|
208
256
|
function limitBytes(maxBytes) {
|
|
209
257
|
let size = 0;
|
|
@@ -277,6 +325,9 @@ export function createBeauticodeUi({
|
|
|
277
325
|
const pendingSelections = new Map();
|
|
278
326
|
let restoreStarted = false;
|
|
279
327
|
let pickerBusy = false;
|
|
328
|
+
let activePickerAbort = null;
|
|
329
|
+
let activePickerResponse = null;
|
|
330
|
+
let disposed = false;
|
|
280
331
|
|
|
281
332
|
function pruneSelections() {
|
|
282
333
|
const timestamp = now();
|
|
@@ -336,6 +387,12 @@ export function createBeauticodeUi({
|
|
|
336
387
|
|
|
337
388
|
return {
|
|
338
389
|
dispose() {
|
|
390
|
+
disposed = true;
|
|
391
|
+
activePickerAbort?.abort();
|
|
392
|
+
activePickerResponse?.destroy();
|
|
393
|
+
activePickerAbort = null;
|
|
394
|
+
activePickerResponse = null;
|
|
395
|
+
pickerBusy = false;
|
|
339
396
|
pendingSelections.clear();
|
|
340
397
|
return stopInProcessSession(dataRoot);
|
|
341
398
|
},
|
|
@@ -485,9 +542,15 @@ export function createBeauticodeUi({
|
|
|
485
542
|
sendJson(res, 409, { ok: false, error: "文件选择器已经打开。" });
|
|
486
543
|
return;
|
|
487
544
|
}
|
|
545
|
+
if (disposed) {
|
|
546
|
+
sendJson(res, 503, { ok: false, error: "beautiCode UI 已停止。" });
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
488
549
|
pickerBusy = true;
|
|
489
550
|
const pickerStartedAt = performance.now();
|
|
490
551
|
const pickerAbort = new AbortController();
|
|
552
|
+
activePickerAbort = pickerAbort;
|
|
553
|
+
activePickerResponse = res;
|
|
491
554
|
const onResponseClose = () => {
|
|
492
555
|
if (!res.writableEnded) pickerAbort.abort();
|
|
493
556
|
};
|
|
@@ -517,6 +580,8 @@ export function createBeauticodeUi({
|
|
|
517
580
|
});
|
|
518
581
|
} finally {
|
|
519
582
|
res.off("close", onResponseClose);
|
|
583
|
+
if (activePickerAbort === pickerAbort) activePickerAbort = null;
|
|
584
|
+
if (activePickerResponse === res) activePickerResponse = null;
|
|
520
585
|
pickerBusy = false;
|
|
521
586
|
}
|
|
522
587
|
},
|