buddy-workbench 0.2.3 → 0.2.5
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/server/routes/jira-filters.js +30 -2
- package/server/services/clipboard-history.js +108 -49
- package/server/services/mac-clipboard-helper.swift +20 -11
- package/ui/dist/assets/index-CaJw9iww.css +1 -0
- package/ui/dist/assets/index-Dv5RdD7S.js +622 -0
- package/ui/dist/assets/{xmindAdapter-Bu9WO4K1.js → xmindAdapter-D7dXikjo.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
- package/ui/dist/assets/index-DVXjDLbz.js +0 -622
- package/ui/dist/assets/index-mbeoDHzc.css +0 -1
package/package.json
CHANGED
|
@@ -328,6 +328,31 @@ async function searchJiraWithFallbacks(jiraHost, headers, jqlCandidates, fields
|
|
|
328
328
|
return [];
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
async function searchJiraWithFallbacksPaginated(jiraHost, headers, jqlCandidates, fields, maxResults = 100) {
|
|
332
|
+
for (const jql of jqlCandidates) {
|
|
333
|
+
if (!jql) continue;
|
|
334
|
+
try {
|
|
335
|
+
const issues = [];
|
|
336
|
+
let startAt = 0;
|
|
337
|
+
let total = 0;
|
|
338
|
+
do {
|
|
339
|
+
const searchUrl = `https://${jiraHost}/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${encodeURIComponent(fields)}`;
|
|
340
|
+
const response = await httpClient.get(searchUrl, { headers });
|
|
341
|
+
if (response.status !== 200 || !Array.isArray(response.data?.issues)) throw new Error(`Jira search returned ${response.status}`);
|
|
342
|
+
const page = response.data.issues;
|
|
343
|
+
issues.push(...page);
|
|
344
|
+
total = Number(response.data.total) || issues.length;
|
|
345
|
+
startAt += page.length;
|
|
346
|
+
if (page.length === 0) break;
|
|
347
|
+
} while (startAt < total);
|
|
348
|
+
return issues;
|
|
349
|
+
} catch {
|
|
350
|
+
// Try the next JQL form when this Jira instance does not support the current one.
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
|
|
331
356
|
async function fetchCommentsForIssue(jiraHost, headers, issueKey, maxResults = 50) {
|
|
332
357
|
try {
|
|
333
358
|
const url = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}/comment?maxResults=${maxResults}`;
|
|
@@ -365,6 +390,9 @@ router.get('/recently-commented', async (_req, res) => {
|
|
|
365
390
|
const identity = await getJiraIdentity(jiraHost, token);
|
|
366
391
|
|
|
367
392
|
const jqlCandidates = [];
|
|
393
|
+
// This function targets issues where the current user authored a comment. The
|
|
394
|
+
// text search fallbacks below can miss comments that do not mention the author.
|
|
395
|
+
jqlCandidates.push('commentedBy(currentUser()) ORDER BY updated DESC');
|
|
368
396
|
if (identity.name) {
|
|
369
397
|
jqlCandidates.push(`comment ~ "\\"${identity.name}\\"" ORDER BY updated DESC`);
|
|
370
398
|
jqlCandidates.push(`comment ~ "${identity.name}" ORDER BY updated DESC`);
|
|
@@ -378,10 +406,10 @@ router.get('/recently-commented', async (_req, res) => {
|
|
|
378
406
|
jqlCandidates.push('(assignee = currentUser() OR reporter = currentUser() OR watcher = currentUser()) AND updated >= -60d ORDER BY updated DESC');
|
|
379
407
|
jqlCandidates.push('updated >= -60d ORDER BY updated DESC');
|
|
380
408
|
|
|
381
|
-
const issues = await
|
|
409
|
+
const issues = await searchJiraWithFallbacksPaginated(jiraHost, headers, jqlCandidates, 'summary,priority,status,updated,assignee,reporter');
|
|
382
410
|
const results = [];
|
|
383
411
|
|
|
384
|
-
const issueCommentsPromises = issues.
|
|
412
|
+
const issueCommentsPromises = issues.map(async (issue) => {
|
|
385
413
|
const key = issue.key;
|
|
386
414
|
const fields = issue.fields || {};
|
|
387
415
|
const comments = await fetchCommentsForIssue(jiraHost, headers, key, 50);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -23,7 +23,10 @@ let suppressImageCaptureUntil = 0;
|
|
|
23
23
|
let swiftClipboardRetryAt = 0;
|
|
24
24
|
let mozjpegWasmModule = null;
|
|
25
25
|
let macClipboardHelper = null;
|
|
26
|
+
let macClipboardHelperStarting = false;
|
|
26
27
|
let macClipboardHelperRestartTimer = null;
|
|
28
|
+
let macClipboardHelperRestartDelay = 5000;
|
|
29
|
+
let macClipboardHelperBinary = null;
|
|
27
30
|
let macClipboardHelperWatchdog = null;
|
|
28
31
|
let macClipboardHelperExitHookRegistered = false;
|
|
29
32
|
const clipboardErrorCounts = new Map();
|
|
@@ -64,7 +67,7 @@ function logClipboardError(stage, error, threshold = 3) {
|
|
|
64
67
|
|
|
65
68
|
async function compressMozjpeg(rgbaBuf, width, height, quality = 75) {
|
|
66
69
|
if (!mozjpegWasmModule) {
|
|
67
|
-
const wasmPath = fileURLToPath(import.meta.resolve('
|
|
70
|
+
const wasmPath = fileURLToPath(import.meta.resolve('@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm'));
|
|
68
71
|
mozjpegWasmModule = await WebAssembly.compile(readFileSync(wasmPath));
|
|
69
72
|
await initMozjpeg(mozjpegWasmModule);
|
|
70
73
|
}
|
|
@@ -229,6 +232,7 @@ function handleMacClipboardHelperMessage(state, line) {
|
|
|
229
232
|
|
|
230
233
|
if (message.kind === 'ready') {
|
|
231
234
|
state.ready = true;
|
|
235
|
+
macClipboardHelperRestartDelay = 5000;
|
|
232
236
|
markClipboardSuccess('native image helper startup', 'native image helper process');
|
|
233
237
|
return;
|
|
234
238
|
}
|
|
@@ -246,7 +250,7 @@ function handleMacClipboardHelperMessage(state, line) {
|
|
|
246
250
|
if (message.kind !== 'image') return;
|
|
247
251
|
|
|
248
252
|
const filename = String(message.filename || '');
|
|
249
|
-
const validFilename = /^capture-[0-9a-f-]+\.
|
|
253
|
+
const validFilename = /^capture-[0-9a-f-]+\.rgba$/i.test(filename);
|
|
250
254
|
if (validFilename && Date.now() < suppressImageCaptureUntil) {
|
|
251
255
|
try { unlinkSync(join(paths.clipboardDir, 'content', filename)); } catch {}
|
|
252
256
|
return;
|
|
@@ -259,40 +263,44 @@ function handleMacClipboardHelperMessage(state, line) {
|
|
|
259
263
|
|| !Number.isInteger(width) || width <= 0
|
|
260
264
|
|| !Number.isInteger(height) || height <= 0
|
|
261
265
|
|| width * height > 40_000_000
|
|
262
|
-
|| !Number.isInteger(reportedBytes) || reportedBytes
|
|
266
|
+
|| !Number.isInteger(reportedBytes) || reportedBytes !== width * height * 4) {
|
|
263
267
|
if (tempPath) { try { unlinkSync(tempPath); } catch {} }
|
|
264
268
|
logClipboardError('native image helper protocol', new Error('Native image helper returned invalid image metadata.'));
|
|
265
269
|
return;
|
|
266
270
|
}
|
|
267
271
|
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
272
|
+
const processImage = async () => {
|
|
273
|
+
const processingStartedAt = Date.now();
|
|
274
|
+
try {
|
|
275
|
+
const rgba = readFileSync(tempPath);
|
|
276
|
+
if (rgba.length !== reportedBytes) throw new Error('Temporary RGBA size did not match the helper metadata.');
|
|
277
|
+
const hash = createHash('md5').update(rgba).digest('hex');
|
|
278
|
+
const jpeg = await compressMozjpeg(rgba, width, height, 75);
|
|
279
|
+
persistCapturedClipboardImage(jpeg, hash, 'jpg');
|
|
280
|
+
const captureMs = Number(message.elapsedMs);
|
|
281
|
+
const totalMs = (Number.isFinite(captureMs) ? captureMs : 0) + Date.now() - processingStartedAt;
|
|
282
|
+
if (totalMs >= 3000) {
|
|
283
|
+
addErrorRecord({
|
|
284
|
+
source: 'Clipboard Monitor',
|
|
285
|
+
level: 'warning',
|
|
286
|
+
message: `Clipboard image capture took ${totalMs} ms.`,
|
|
287
|
+
details: `${width} × ${height}, source type ${String(message.sourceType || 'unknown')}, MozJPEG ${jpeg.length} bytes.`
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
markClipboardSuccess(
|
|
291
|
+
'native clipboard pasteboard read',
|
|
292
|
+
'native clipboard image validation',
|
|
293
|
+
'native clipboard image decode',
|
|
294
|
+
'native clipboard JPEG encode',
|
|
295
|
+
'native clipboard JPEG save'
|
|
296
|
+
);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
logClipboardError('native clipboard MozJPEG encode', error);
|
|
299
|
+
} finally {
|
|
300
|
+
try { unlinkSync(tempPath); } catch {}
|
|
283
301
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
'native clipboard image validation',
|
|
287
|
-
'native clipboard image decode',
|
|
288
|
-
'native clipboard JPEG encode',
|
|
289
|
-
'native clipboard JPEG save'
|
|
290
|
-
);
|
|
291
|
-
} catch (error) {
|
|
292
|
-
logClipboardError('native clipboard JPEG handoff', error);
|
|
293
|
-
} finally {
|
|
294
|
-
try { unlinkSync(tempPath); } catch {}
|
|
295
|
-
}
|
|
302
|
+
};
|
|
303
|
+
state.imageProcessing = (state.imageProcessing || Promise.resolve()).then(processImage);
|
|
296
304
|
}
|
|
297
305
|
|
|
298
306
|
function scheduleMacClipboardHelperRestart() {
|
|
@@ -300,7 +308,8 @@ function scheduleMacClipboardHelperRestart() {
|
|
|
300
308
|
macClipboardHelperRestartTimer = setTimeout(() => {
|
|
301
309
|
macClipboardHelperRestartTimer = null;
|
|
302
310
|
startMacClipboardImageHelper();
|
|
303
|
-
},
|
|
311
|
+
}, macClipboardHelperRestartDelay);
|
|
312
|
+
macClipboardHelperRestartDelay = Math.min(macClipboardHelperRestartDelay * 2, 5 * 60 * 1000);
|
|
304
313
|
macClipboardHelperRestartTimer.unref();
|
|
305
314
|
}
|
|
306
315
|
|
|
@@ -315,7 +324,7 @@ function stopMacClipboardImageHelper() {
|
|
|
315
324
|
}
|
|
316
325
|
|
|
317
326
|
function startMacClipboardImageHelper() {
|
|
318
|
-
if (process.platform !== 'darwin' || macClipboardHelper || macClipboardHelperRestartTimer) return;
|
|
327
|
+
if (process.platform !== 'darwin' || macClipboardHelper || macClipboardHelperStarting || macClipboardHelperRestartTimer) return;
|
|
319
328
|
const outputDirectory = join(paths.clipboardDir, 'content');
|
|
320
329
|
mkdirSync(outputDirectory, { recursive: true, mode: 0o700 });
|
|
321
330
|
try { chmodSync(outputDirectory, 0o700); } catch (error) {
|
|
@@ -323,16 +332,40 @@ function startMacClipboardImageHelper() {
|
|
|
323
332
|
return;
|
|
324
333
|
}
|
|
325
334
|
const scriptPath = fileURLToPath(new URL('./mac-clipboard-helper.swift', import.meta.url));
|
|
335
|
+
const binaryPath = macClipboardHelperBinary || join(outputDirectory, 'mac-clipboard-helper');
|
|
336
|
+
macClipboardHelperStarting = true;
|
|
337
|
+
void (async () => {
|
|
338
|
+
try {
|
|
339
|
+
if (!macClipboardHelperBinary) {
|
|
340
|
+
await execFileAsync('swiftc', ['-O', scriptPath, '-o', binaryPath], {
|
|
341
|
+
timeout: 120 * 1000,
|
|
342
|
+
maxBuffer: 10 * 1024 * 1024
|
|
343
|
+
});
|
|
344
|
+
chmodSync(binaryPath, 0o700);
|
|
345
|
+
macClipboardHelperBinary = binaryPath;
|
|
346
|
+
}
|
|
347
|
+
launchMacClipboardImageHelper(binaryPath, outputDirectory);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
logClipboardError('native image helper compile', error);
|
|
350
|
+
scheduleMacClipboardHelperRestart();
|
|
351
|
+
} finally {
|
|
352
|
+
macClipboardHelperStarting = false;
|
|
353
|
+
}
|
|
354
|
+
})();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function launchMacClipboardImageHelper(binaryPath, outputDirectory) {
|
|
326
358
|
let child;
|
|
327
359
|
try {
|
|
328
|
-
child = spawn(
|
|
360
|
+
child = spawn(binaryPath, [outputDirectory], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
329
361
|
} catch (error) {
|
|
330
362
|
logClipboardError('native image helper startup', error);
|
|
363
|
+
macClipboardHelperBinary = null;
|
|
331
364
|
scheduleMacClipboardHelperRestart();
|
|
332
365
|
return;
|
|
333
366
|
}
|
|
334
367
|
|
|
335
|
-
const state = { child, ready: false, lastMessageAt: Date.now(), lineBuffer: '', stderr: '' };
|
|
368
|
+
const state = { child, ready: false, lastMessageAt: Date.now(), lineBuffer: '', stderr: '', imageProcessing: Promise.resolve() };
|
|
336
369
|
macClipboardHelper = state;
|
|
337
370
|
child.stdout.setEncoding('utf8');
|
|
338
371
|
child.stdout.on('data', (chunk) => {
|
|
@@ -363,6 +396,7 @@ function startMacClipboardImageHelper() {
|
|
|
363
396
|
const details = state.stderr.trim();
|
|
364
397
|
logClipboardError('native image helper process', new Error(details || `Helper exited with code ${code ?? signal ?? 'unknown'}.`));
|
|
365
398
|
}
|
|
399
|
+
if (!state.ready) macClipboardHelperBinary = null;
|
|
366
400
|
scheduleMacClipboardHelperRestart();
|
|
367
401
|
});
|
|
368
402
|
|
|
@@ -430,11 +464,17 @@ function getClipboardEditorDetails() {
|
|
|
430
464
|
}
|
|
431
465
|
|
|
432
466
|
function withClipboardEditorDetails(item, date, editorDetails) {
|
|
433
|
-
const
|
|
434
|
-
|
|
467
|
+
const itemWithMetadata = {
|
|
468
|
+
...item,
|
|
469
|
+
...(date ? { date } : {}),
|
|
470
|
+
...(item.imageFile ? { imageSize: (() => {
|
|
471
|
+
try { return statSync(clipboardImagePath(item.imageFile)).size; } catch { return null; }
|
|
472
|
+
})() } : {})
|
|
473
|
+
};
|
|
474
|
+
if (!item.contentFile) return itemWithMetadata;
|
|
435
475
|
const { editor, editorName } = editorDetails || getClipboardEditorDetails();
|
|
436
476
|
return {
|
|
437
|
-
...
|
|
477
|
+
...itemWithMetadata,
|
|
438
478
|
editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
|
|
439
479
|
editorName
|
|
440
480
|
};
|
|
@@ -499,16 +539,7 @@ export function allTaggedClipboardItems() {
|
|
|
499
539
|
const items = dayItems(date);
|
|
500
540
|
for (const item of items) {
|
|
501
541
|
if (Array.isArray(item.tags) && item.tags.length > 0) {
|
|
502
|
-
result.push(
|
|
503
|
-
item.contentFile
|
|
504
|
-
? {
|
|
505
|
-
...item,
|
|
506
|
-
date,
|
|
507
|
-
editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
|
|
508
|
-
editorName
|
|
509
|
-
}
|
|
510
|
-
: { ...item, date }
|
|
511
|
-
);
|
|
542
|
+
result.push(withClipboardEditorDetails(item, date, { editor, editorName }));
|
|
512
543
|
}
|
|
513
544
|
}
|
|
514
545
|
}
|
|
@@ -654,6 +685,7 @@ export function startClipboardCapture() {
|
|
|
654
685
|
}
|
|
655
686
|
|
|
656
687
|
export async function markClipboardCopied(date, id) {
|
|
688
|
+
let textToCopy = null;
|
|
657
689
|
let targetDate = date;
|
|
658
690
|
let items = dayItems(targetDate);
|
|
659
691
|
let item = items.find((entry) => entry.id === id);
|
|
@@ -685,10 +717,36 @@ export async function markClipboardCopied(date, id) {
|
|
|
685
717
|
} else {
|
|
686
718
|
const text = clipboardOriginal(targetDate, id) || item.text || item.preview || '';
|
|
687
719
|
if (text) {
|
|
720
|
+
textToCopy = text;
|
|
688
721
|
lastValue = text.trim();
|
|
722
|
+
if (targetDate !== today()) {
|
|
723
|
+
const todayDate = today();
|
|
724
|
+
const todayItems = dayItems(todayDate);
|
|
725
|
+
const alreadyInToday = todayItems.some((entry) => {
|
|
726
|
+
if (entry.text !== undefined) return entry.text === text;
|
|
727
|
+
if (!entry.contentFile) return false;
|
|
728
|
+
try {
|
|
729
|
+
return readFileSync(join(paths.clipboardDir, 'content', entry.contentFile), 'utf8') === text;
|
|
730
|
+
} catch {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
if (!alreadyInToday) {
|
|
735
|
+
const newId = randomUUID();
|
|
736
|
+
const isLong = text.length > previewLimit;
|
|
737
|
+
if (isLong) {
|
|
738
|
+
mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true });
|
|
739
|
+
writeFileSync(join(paths.clipboardDir, 'content', newId), text, 'utf8');
|
|
740
|
+
}
|
|
741
|
+
const copiedItem = isLong
|
|
742
|
+
? { id: newId, preview: `${text.slice(0, previewLimit)}…`, contentFile: newId, createdAt: new Date().toISOString() }
|
|
743
|
+
: { id: newId, text, createdAt: new Date().toISOString() };
|
|
744
|
+
writeJson(dayFile(todayDate), [copiedItem, ...todayItems].slice(0, 200));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
689
747
|
}
|
|
690
748
|
}
|
|
691
|
-
return item;
|
|
749
|
+
return textToCopy === null ? item : { ...item, copyText: textToCopy };
|
|
692
750
|
}
|
|
693
751
|
|
|
694
752
|
export async function copyClipboardImage(date, id) {
|
|
@@ -735,6 +793,7 @@ export function saveClipboardImage(buffer, ext = 'png', imageHash = '') {
|
|
|
735
793
|
const item = {
|
|
736
794
|
id,
|
|
737
795
|
imageFile: filename,
|
|
796
|
+
imageSize: buffer.length,
|
|
738
797
|
imageHash: hash,
|
|
739
798
|
fileHash,
|
|
740
799
|
imageHashes: Array.from(new Set([hash, fileHash].filter(Boolean))),
|
|
@@ -63,21 +63,30 @@ func captureImage(_ pasteboard: NSPasteboard, changeCount: Int, startedAt: Date)
|
|
|
63
63
|
return .complete
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
let bytesPerRow = width * 4
|
|
67
|
+
var rgba = Data(count: bytesPerRow * height)
|
|
68
|
+
let rendered = rgba.withUnsafeMutableBytes { buffer -> Bool in
|
|
69
|
+
guard let context = CGContext(
|
|
70
|
+
data: buffer.baseAddress,
|
|
71
|
+
width: width,
|
|
72
|
+
height: height,
|
|
73
|
+
bitsPerComponent: 8,
|
|
74
|
+
bytesPerRow: bytesPerRow,
|
|
75
|
+
space: CGColorSpaceCreateDeviceRGB(),
|
|
76
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
|
|
77
|
+
) else { return false }
|
|
78
|
+
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
79
|
+
return true
|
|
70
80
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
emit(["kind": "error", "stage": "JPEG encode", "message": "ImageIO could not finish JPEG encoding.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
81
|
+
guard rendered else {
|
|
82
|
+
emit(["kind": "error", "stage": "image decode", "message": "Core Graphics could not render clipboard image pixels.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
74
83
|
return .complete
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
let filename = "capture-\(UUID().uuidString).
|
|
86
|
+
let filename = "capture-\(UUID().uuidString).rgba"
|
|
78
87
|
let outputURL = outputDirectory.appendingPathComponent(filename, isDirectory: false)
|
|
79
88
|
do {
|
|
80
|
-
try
|
|
89
|
+
try rgba.write(to: outputURL, options: .atomic)
|
|
81
90
|
try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: outputURL.path)
|
|
82
91
|
emit([
|
|
83
92
|
"kind": "image",
|
|
@@ -85,13 +94,13 @@ func captureImage(_ pasteboard: NSPasteboard, changeCount: Int, startedAt: Date)
|
|
|
85
94
|
"sourceType": type.rawValue,
|
|
86
95
|
"width": width,
|
|
87
96
|
"height": height,
|
|
88
|
-
"bytes":
|
|
97
|
+
"bytes": rgba.count,
|
|
89
98
|
"elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000),
|
|
90
99
|
"filename": filename
|
|
91
100
|
])
|
|
92
101
|
} catch {
|
|
93
102
|
try? FileManager.default.removeItem(at: outputURL)
|
|
94
|
-
emit(["kind": "error", "stage": "
|
|
103
|
+
emit(["kind": "error", "stage": "image save", "message": "Could not write temporary RGBA pixels.", "changeCount": changeCount, "elapsedMs": Int(Date().timeIntervalSince(startedAt) * 1000)])
|
|
95
104
|
}
|
|
96
105
|
return .complete
|
|
97
106
|
}
|