buddy-workbench 0.1.69 → 0.1.71
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/clipboard.js +9 -1
- package/server/routes/jira-filters.js +208 -39
- package/server/routes/updates.js +48 -22
- package/server/services/clipboard-history.js +40 -19
- package/ui/dist/assets/{index-DoX1juq6.js → index-Bu-dHDIQ.js} +64 -64
- package/ui/dist/assets/{index-D5QYI1v4.css → index-CZUbxhFl.css} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import express, { Router } from 'express';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath, allTaggedClipboardItems, updateClipboardItemTags, markClipboardCopied } from '../services/clipboard-history.js';
|
|
3
|
+
import { clipboardDates, clipboardItems, clipboardOriginal, deleteClipboardItem, saveClipboardImage, clipboardImagePath, allTaggedClipboardItems, updateClipboardItemTags, markClipboardCopied, copyClipboardImage } from '../services/clipboard-history.js';
|
|
4
4
|
|
|
5
5
|
const router = Router();
|
|
6
6
|
router.get('/', (req, res) => res.json({ dates: clipboardDates(), items: clipboardItems(req.query.date) }));
|
|
@@ -48,5 +48,13 @@ router.post('/:date/:id/copied', async (req, res) => {
|
|
|
48
48
|
res.status(500).json({ error: error.message });
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
|
+
router.post('/:date/:id/copy-image', async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
await copyClipboardImage(req.params.date, req.params.id);
|
|
54
|
+
res.json({ ok: true });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
res.status(500).json({ error: error.message });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
51
59
|
router.delete('/:date/:id', (req, res) => { if (!deleteClipboardItem(req.params.date, req.params.id)) return res.status(404).json({ error: 'Clipboard entry not found.' }); res.status(204).end(); });
|
|
52
60
|
export default router;
|
|
@@ -54,6 +54,131 @@ async function getJiraIssueSummary(issueKey) {
|
|
|
54
54
|
return summary;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function transformFieldValue(val) {
|
|
58
|
+
if (val === null || val === undefined) return undefined;
|
|
59
|
+
|
|
60
|
+
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
|
61
|
+
return val;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(val)) {
|
|
65
|
+
if (val.length === 0) return undefined;
|
|
66
|
+
const items = val.map((item) => transformFieldValue(item)).filter((item) => item !== undefined);
|
|
67
|
+
return items.length > 0 ? items : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof val === 'object') {
|
|
71
|
+
const keys = Object.keys(val);
|
|
72
|
+
if (keys.length === 0) return undefined;
|
|
73
|
+
|
|
74
|
+
// Support cascading select (parent value + child value)
|
|
75
|
+
if (val.value !== undefined && val.child && typeof val.child === 'object') {
|
|
76
|
+
const childVal = transformFieldValue(val.child);
|
|
77
|
+
if (childVal) {
|
|
78
|
+
return { value: String(val.value), child: childVal };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (val.id !== undefined && val.id !== null) {
|
|
83
|
+
return { id: String(val.id) };
|
|
84
|
+
}
|
|
85
|
+
if (val.accountId !== undefined && val.accountId !== null) {
|
|
86
|
+
return { accountId: String(val.accountId) };
|
|
87
|
+
}
|
|
88
|
+
if (val.value !== undefined && val.value !== null) {
|
|
89
|
+
return { value: String(val.value) };
|
|
90
|
+
}
|
|
91
|
+
if (val.key !== undefined && val.key !== null) {
|
|
92
|
+
return { key: String(val.key) };
|
|
93
|
+
}
|
|
94
|
+
if (val.name !== undefined && val.name !== null) {
|
|
95
|
+
return { name: String(val.name) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractCloneableFields(origFields) {
|
|
103
|
+
if (!origFields || typeof origFields !== 'object') return {};
|
|
104
|
+
|
|
105
|
+
const fields = {};
|
|
106
|
+
|
|
107
|
+
// Standard fields
|
|
108
|
+
if (origFields.description !== undefined && origFields.description !== null) {
|
|
109
|
+
fields.description = origFields.description;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (origFields.priority) {
|
|
113
|
+
if (origFields.priority.id) {
|
|
114
|
+
fields.priority = { id: String(origFields.priority.id) };
|
|
115
|
+
} else if (origFields.priority.name) {
|
|
116
|
+
fields.priority = { name: String(origFields.priority.name) };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (Array.isArray(origFields.components) && origFields.components.length > 0) {
|
|
121
|
+
const components = origFields.components
|
|
122
|
+
.map((c) => (c.id ? { id: String(c.id) } : c.name ? { name: String(c.name) } : null))
|
|
123
|
+
.filter(Boolean);
|
|
124
|
+
if (components.length > 0) fields.components = components;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(origFields.labels) && origFields.labels.length > 0) {
|
|
128
|
+
const labels = origFields.labels.map((l) => String(l)).filter(Boolean);
|
|
129
|
+
if (labels.length > 0) fields.labels = labels;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Array.isArray(origFields.fixVersions) && origFields.fixVersions.length > 0) {
|
|
133
|
+
const fixVersions = origFields.fixVersions
|
|
134
|
+
.map((v) => (v.id ? { id: String(v.id) } : v.name ? { name: String(v.name) } : null))
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
if (fixVersions.length > 0) fields.fixVersions = fixVersions;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(origFields.versions) && origFields.versions.length > 0) {
|
|
140
|
+
const versions = origFields.versions
|
|
141
|
+
.map((v) => (v.id ? { id: String(v.id) } : v.name ? { name: String(v.name) } : null))
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
if (versions.length > 0) fields.versions = versions;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof origFields.environment === 'string' && origFields.environment) {
|
|
147
|
+
fields.environment = origFields.environment;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof origFields.duedate === 'string' && origFields.duedate) {
|
|
151
|
+
fields.duedate = origFields.duedate;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (origFields.security) {
|
|
155
|
+
if (origFields.security.id) fields.security = { id: String(origFields.security.id) };
|
|
156
|
+
else if (origFields.security.name) fields.security = { name: String(origFields.security.name) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (origFields.parent) {
|
|
160
|
+
if (origFields.parent.id) fields.parent = { id: String(origFields.parent.id) };
|
|
161
|
+
else if (origFields.parent.key) fields.parent = { key: String(origFields.parent.key) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (origFields.assignee) {
|
|
165
|
+
if (origFields.assignee.accountId) fields.assignee = { accountId: String(origFields.assignee.accountId) };
|
|
166
|
+
else if (origFields.assignee.name) fields.assignee = { name: String(origFields.assignee.name) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Custom fields starting with customfield_
|
|
170
|
+
for (const [key, val] of Object.entries(origFields)) {
|
|
171
|
+
if (key.startsWith('customfield_') && val !== null && val !== undefined) {
|
|
172
|
+
const transformed = transformFieldValue(val);
|
|
173
|
+
if (transformed !== undefined) {
|
|
174
|
+
fields[key] = transformed;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return fields;
|
|
180
|
+
}
|
|
181
|
+
|
|
57
182
|
// List all filters
|
|
58
183
|
router.get('/', (_req, res) => {
|
|
59
184
|
res.json(listJiraFilters());
|
|
@@ -271,12 +396,11 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
271
396
|
recordApiError({ source: 'Jira API (Clone Issue)', method: 'GET', url: myselfUrl, message: error.message || 'Failed to fetch current user.' });
|
|
272
397
|
}
|
|
273
398
|
|
|
274
|
-
// Fetch original issue details to preserve issue type, project, description, priority
|
|
399
|
+
// Fetch original issue details to preserve issue type, project, description, priority, components, labels, fixVersions, versions, environment, duedate, security, parent, custom fields
|
|
400
|
+
let origFields = null;
|
|
275
401
|
let origIssueType = null;
|
|
276
402
|
let origProjectKey = null;
|
|
277
403
|
let origSummary = null;
|
|
278
|
-
let origDescription = null;
|
|
279
|
-
let origPriority = null;
|
|
280
404
|
|
|
281
405
|
if (issueKey) {
|
|
282
406
|
const issueUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(issueKey)}`;
|
|
@@ -286,29 +410,19 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
286
410
|
recordApiError({ source: 'Jira API (Clone Issue)', method: 'GET', url: issueUrl, status: origRes.status, message: `Jira issue API returned ${origRes.status}: ${origRes.statusText || 'Failed to fetch original issue.'}` });
|
|
287
411
|
}
|
|
288
412
|
if (origRes.status === 200 && origRes.data?.fields) {
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
if (
|
|
292
|
-
origIssueType = { id:
|
|
293
|
-
} else if (
|
|
294
|
-
origIssueType = { name:
|
|
413
|
+
origFields = origRes.data.fields;
|
|
414
|
+
if (origFields.issuetype) {
|
|
415
|
+
if (origFields.issuetype.id) {
|
|
416
|
+
origIssueType = { id: String(origFields.issuetype.id) };
|
|
417
|
+
} else if (origFields.issuetype.name) {
|
|
418
|
+
origIssueType = { name: String(origFields.issuetype.name) };
|
|
295
419
|
}
|
|
296
420
|
}
|
|
297
|
-
if (
|
|
298
|
-
origProjectKey =
|
|
299
|
-
}
|
|
300
|
-
if (fields.summary) {
|
|
301
|
-
origSummary = String(fields.summary).trim();
|
|
421
|
+
if (origFields.project?.key) {
|
|
422
|
+
origProjectKey = origFields.project.key;
|
|
302
423
|
}
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
if (fields.priority) {
|
|
307
|
-
if (fields.priority.id) {
|
|
308
|
-
origPriority = { id: fields.priority.id };
|
|
309
|
-
} else if (fields.priority.name) {
|
|
310
|
-
origPriority = { name: fields.priority.name };
|
|
311
|
-
}
|
|
424
|
+
if (origFields.summary) {
|
|
425
|
+
origSummary = String(origFields.summary).trim();
|
|
312
426
|
}
|
|
313
427
|
}
|
|
314
428
|
} catch (error) {
|
|
@@ -321,26 +435,60 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
321
435
|
const projectKey = origProjectKey || (issueKey ? issueKey.split('-')[0] : '');
|
|
322
436
|
const requestedSummary = summary.trim();
|
|
323
437
|
const cloneSummary = origSummary || requestedSummary;
|
|
438
|
+
|
|
439
|
+
// Extract all cloneable standard & custom fields from original issue
|
|
440
|
+
const cloneableFields = extractCloneableFields(origFields);
|
|
441
|
+
|
|
442
|
+
// Override assignee with current authenticated user if resolved
|
|
443
|
+
if (assigneeField) {
|
|
444
|
+
cloneableFields.assignee = assigneeField;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Core fields required for issue creation
|
|
448
|
+
const coreFields = {
|
|
449
|
+
summary: cloneSummary,
|
|
450
|
+
...(projectKey ? { project: { key: projectKey } } : {}),
|
|
451
|
+
issuetype: targetIssueType
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
let activeFields = { ...cloneableFields, ...coreFields };
|
|
455
|
+
let pendingPutFields = {};
|
|
456
|
+
|
|
324
457
|
const url = `https://${jiraHost}/rest/api/2/issue`;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
458
|
+
let response = await httpClient.post(url, { fields: activeFields }, { headers });
|
|
459
|
+
|
|
460
|
+
// Handle field rejection on POST creation (e.g. fields not configured on Create Screen)
|
|
461
|
+
let maxRetries = 3;
|
|
462
|
+
while (response.status !== 201 && response.status !== 200 && maxRetries > 0) {
|
|
463
|
+
maxRetries--;
|
|
464
|
+
const errFields = response.data?.errors ? Object.keys(response.data.errors) : [];
|
|
465
|
+
if (errFields.length > 0) {
|
|
466
|
+
let strippedAny = false;
|
|
467
|
+
for (const errField of errFields) {
|
|
468
|
+
if (activeFields[errField] !== undefined && !['summary', 'project', 'issuetype'].includes(errField)) {
|
|
469
|
+
pendingPutFields[errField] = activeFields[errField];
|
|
470
|
+
delete activeFields[errField];
|
|
471
|
+
strippedAny = true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (strippedAny) {
|
|
475
|
+
response = await httpClient.post(url, { fields: activeFields }, { headers });
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
333
478
|
}
|
|
334
|
-
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
335
481
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
482
|
+
// Fallback attempt with minimal core fields if POST creation still failed
|
|
483
|
+
if (response.status !== 201 && response.status !== 200) {
|
|
484
|
+
for (const [k, v] of Object.entries(activeFields)) {
|
|
485
|
+
if (!['summary', 'project', 'issuetype'].includes(k)) {
|
|
486
|
+
pendingPutFields[k] = v;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
339
489
|
const fallbackPayload = {
|
|
340
490
|
fields: {
|
|
341
|
-
|
|
342
|
-
...(projectKey ? { project: { key: projectKey } } : {}),
|
|
343
|
-
issuetype: targetIssueType,
|
|
491
|
+
...coreFields,
|
|
344
492
|
...(assigneeField ? { assignee: assigneeField } : {})
|
|
345
493
|
}
|
|
346
494
|
};
|
|
@@ -358,7 +506,28 @@ router.post('/issues/clone', async (req, res) => {
|
|
|
358
506
|
const created = response.data || {};
|
|
359
507
|
const createdKey = created.key || issueKey;
|
|
360
508
|
|
|
361
|
-
//
|
|
509
|
+
// Apply pending fields via PUT update if any fields could not be set on creation screen
|
|
510
|
+
if (createdKey && Object.keys(pendingPutFields).length > 0) {
|
|
511
|
+
const updateUrl = `https://${jiraHost}/rest/api/2/issue/${encodeURIComponent(createdKey)}`;
|
|
512
|
+
try {
|
|
513
|
+
let putRes = await httpClient.put(updateUrl, { fields: pendingPutFields }, { headers });
|
|
514
|
+
if (putRes.status < 200 || putRes.status >= 300) {
|
|
515
|
+
const putErrFields = putRes.data?.errors ? Object.keys(putRes.data.errors) : [];
|
|
516
|
+
if (putErrFields.length > 0) {
|
|
517
|
+
for (const ef of putErrFields) {
|
|
518
|
+
delete pendingPutFields[ef];
|
|
519
|
+
}
|
|
520
|
+
if (Object.keys(pendingPutFields).length > 0) {
|
|
521
|
+
await httpClient.put(updateUrl, { fields: pendingPutFields }, { headers });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
recordApiError({ source: 'Jira API (Clone Issue PUT fields)', method: 'PUT', url: `https://${jiraHost}/rest/api/2/issue/${createdKey}`, message: err.message || 'Failed to PUT cloned fields.' });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Fallback: Assign explicitly if not assigned during creation/PUT
|
|
362
531
|
if (createdKey && assigneeField) {
|
|
363
532
|
const assignUrl = `https://${jiraHost}/rest/api/2/issue/${createdKey}/assignee`;
|
|
364
533
|
try {
|
package/server/routes/updates.js
CHANGED
|
@@ -83,29 +83,55 @@ function buildReleaseList(metadata) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
async function readNpmPackageMetadata() {
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
86
|
+
// npm knows about the user's ~/.npmrc, including private registries,
|
|
87
|
+
// proxies, authentication and custom CA settings. This matters in the
|
|
88
|
+
// internal-network desktop build, where those settings are not necessarily
|
|
89
|
+
// exposed as npm_config_* environment variables.
|
|
90
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
91
|
+
try {
|
|
92
|
+
const { stdout } = await execFileAsync(
|
|
93
|
+
npmCommand,
|
|
94
|
+
['view', `${PACKAGE_NAME}@latest`, 'version', 'time', 'devbuddyChangelog', '--json'],
|
|
95
|
+
{
|
|
96
|
+
timeout: 8000,
|
|
97
|
+
maxBuffer: 1024 * 1024,
|
|
98
|
+
shell: process.platform === 'win32',
|
|
99
|
+
windowsHide: true
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
const metadata = JSON.parse(stdout);
|
|
103
|
+
if (!metadata || typeof metadata !== 'object' || !metadata.version) {
|
|
104
|
+
throw new Error('The configured npm registry did not return package version metadata.');
|
|
105
|
+
}
|
|
106
|
+
return metadata;
|
|
107
|
+
} catch (npmError) {
|
|
108
|
+
// Keep a fallback for desktop launchers whose reduced PATH cannot find
|
|
109
|
+
// npm. It is intentionally secondary so it cannot bypass ~/.npmrc.
|
|
110
|
+
const registry = String(process.env.npm_config_registry || 'https://registry.npmjs.org/')
|
|
111
|
+
.trim()
|
|
112
|
+
.replace(/\/+$/, '');
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(`${registry}/${encodeURIComponent(PACKAGE_NAME)}`, {
|
|
115
|
+
headers: { Accept: 'application/json' },
|
|
116
|
+
signal: AbortSignal.timeout(8000)
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
throw new Error(`The configured npm registry returned HTTP ${response.status}.`);
|
|
120
|
+
}
|
|
121
|
+
const packageMetadata = await response.json();
|
|
122
|
+
const metadata = {
|
|
123
|
+
version: packageMetadata?.['dist-tags']?.latest,
|
|
124
|
+
time: packageMetadata?.time,
|
|
125
|
+
devbuddyChangelog: packageMetadata?.devbuddyChangelog
|
|
126
|
+
};
|
|
127
|
+
if (!metadata.version) {
|
|
128
|
+
throw new Error('The configured npm registry did not return package version metadata.');
|
|
129
|
+
}
|
|
130
|
+
return metadata;
|
|
131
|
+
} catch {
|
|
132
|
+
throw npmError;
|
|
133
|
+
}
|
|
107
134
|
}
|
|
108
|
-
return metadata;
|
|
109
135
|
}
|
|
110
136
|
|
|
111
137
|
async function checkForUpdates() {
|
|
@@ -13,6 +13,7 @@ const execFileAsync = promisify(execFile);
|
|
|
13
13
|
const previewLimit = 2000;
|
|
14
14
|
let lastValue = '';
|
|
15
15
|
let lastImageHash = '';
|
|
16
|
+
let suppressImageCaptureUntil = 0;
|
|
16
17
|
let mozjpegWasmModule = null;
|
|
17
18
|
|
|
18
19
|
async function compressMozjpeg(rgbaBuf, width, height, quality = 75) {
|
|
@@ -304,7 +305,7 @@ export async function captureClipboard() {
|
|
|
304
305
|
} catch {}
|
|
305
306
|
|
|
306
307
|
// 2. Image capture (Mac only & clipboardImageEnabled !== false)
|
|
307
|
-
if (process.platform === 'darwin' && settings.clipboardImageEnabled !== false) {
|
|
308
|
+
if (process.platform === 'darwin' && settings.clipboardImageEnabled !== false && Date.now() >= suppressImageCaptureUntil) {
|
|
308
309
|
try {
|
|
309
310
|
const imageData = await getMacClipboardImageData();
|
|
310
311
|
if (imageData && imageData.rgbaBuf.length > 0) {
|
|
@@ -389,24 +390,13 @@ export async function markClipboardCopied(date, id) {
|
|
|
389
390
|
if (!item) return null;
|
|
390
391
|
|
|
391
392
|
if (item.imageFile) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
? item.imageHashes
|
|
400
|
-
: [item.imageHash, item.fileHash].filter(Boolean);
|
|
401
|
-
|
|
402
|
-
if (!existingHashes.includes(currentHash)) {
|
|
403
|
-
const updatedItem = { ...item, imageHashes: [...existingHashes, currentHash] };
|
|
404
|
-
const nextItems = items.map((entry) => (entry.id === id ? updatedItem : entry));
|
|
405
|
-
writeJson(dayFile(targetDate), nextItems);
|
|
406
|
-
return updatedItem;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
} catch {}
|
|
393
|
+
// Set the monitor's last hash before the browser writes the image. This
|
|
394
|
+
// prevents copying an existing image from being captured as a new item.
|
|
395
|
+
suppressImageCaptureUntil = Date.now() + 5000;
|
|
396
|
+
const existingHashes = Array.isArray(item.imageHashes)
|
|
397
|
+
? item.imageHashes
|
|
398
|
+
: [item.imageHash, item.fileHash].filter(Boolean);
|
|
399
|
+
lastImageHash = existingHashes[0] || '';
|
|
410
400
|
} else {
|
|
411
401
|
const text = clipboardOriginal(targetDate, id) || item.text || item.preview || '';
|
|
412
402
|
if (text) {
|
|
@@ -416,6 +406,37 @@ export async function markClipboardCopied(date, id) {
|
|
|
416
406
|
return item;
|
|
417
407
|
}
|
|
418
408
|
|
|
409
|
+
export async function copyClipboardImage(date, id) {
|
|
410
|
+
if (process.platform !== 'darwin') throw new Error('Native image clipboard is only supported on macOS.');
|
|
411
|
+
|
|
412
|
+
let item = dayItems(date).find((entry) => entry.id === id);
|
|
413
|
+
if (!item) {
|
|
414
|
+
for (const d of clipboardDates()) {
|
|
415
|
+
item = dayItems(d).find((entry) => entry.id === id);
|
|
416
|
+
if (item) break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!item?.imageFile) throw new Error('Clipboard image entry not found.');
|
|
420
|
+
|
|
421
|
+
await markClipboardCopied(date, id);
|
|
422
|
+
const imagePath = clipboardImagePath(item.imageFile);
|
|
423
|
+
const swiftCode = `
|
|
424
|
+
import Cocoa
|
|
425
|
+
let path = CommandLine.arguments[1]
|
|
426
|
+
if let image = NSImage(contentsOfFile: path),
|
|
427
|
+
let tiff = image.tiffRepresentation,
|
|
428
|
+
let rep = NSBitmapImageRep(data: tiff),
|
|
429
|
+
let png = rep.representation(using: .png, properties: [:]) {
|
|
430
|
+
let pasteboard = NSPasteboard.general
|
|
431
|
+
pasteboard.clearContents()
|
|
432
|
+
if !pasteboard.setData(png, forType: .png) { exit(1) }
|
|
433
|
+
} else {
|
|
434
|
+
exit(1)
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
437
|
+
await execFileAsync('swift', ['-e', swiftCode, imagePath], { timeout: 5000 });
|
|
438
|
+
}
|
|
439
|
+
|
|
419
440
|
export function saveClipboardImage(buffer, ext = 'png', imageHash = '') {
|
|
420
441
|
const date = today();
|
|
421
442
|
const id = randomUUID();
|