pinokiod 3.193.0 → 3.195.0
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/views/shell.ejs +256 -37
- package/server/views/terminal.ejs +234 -76
package/package.json
CHANGED
package/server/views/shell.ejs
CHANGED
|
@@ -1163,20 +1163,216 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1163
1163
|
dropOverlay.className = "terminal-drop-overlay"
|
|
1164
1164
|
dropOverlay.textContent = "Drop files to upload"
|
|
1165
1165
|
terminalContainer.appendChild(dropOverlay)
|
|
1166
|
-
const
|
|
1166
|
+
const dedupeClipboardFiles = (inputs) => {
|
|
1167
1167
|
const seen = new Set()
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1168
|
+
const results = []
|
|
1169
|
+
inputs.forEach((file) => {
|
|
1170
|
+
if (!file || !(file instanceof File) || !(file.size > 0)) {
|
|
1171
|
+
return
|
|
1171
1172
|
}
|
|
1172
|
-
const
|
|
1173
|
-
if (
|
|
1174
|
-
return
|
|
1173
|
+
const signature = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
|
|
1174
|
+
if (seen.has(signature)) {
|
|
1175
|
+
return
|
|
1176
|
+
}
|
|
1177
|
+
seen.add(signature)
|
|
1178
|
+
results.push(file)
|
|
1179
|
+
})
|
|
1180
|
+
return results
|
|
1181
|
+
}
|
|
1182
|
+
const collectClipboardFiles = (clipboardData) => {
|
|
1183
|
+
const files = []
|
|
1184
|
+
const seen = new Set()
|
|
1185
|
+
if (!clipboardData) {
|
|
1186
|
+
return { files, hasFileFlavor: false }
|
|
1187
|
+
}
|
|
1188
|
+
let hasFileFlavor = false
|
|
1189
|
+
try {
|
|
1190
|
+
const types = Array.from(clipboardData.types || [])
|
|
1191
|
+
hasFileFlavor = types.some((type) => type === "Files" || type === "application/x-moz-file")
|
|
1192
|
+
} catch (_) {}
|
|
1193
|
+
const pushIfUnique = (file) => {
|
|
1194
|
+
if (!file || !(file instanceof File) || !(file.size > 0)) {
|
|
1195
|
+
return
|
|
1196
|
+
}
|
|
1197
|
+
const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
|
|
1198
|
+
if (seen.has(key)) {
|
|
1199
|
+
return
|
|
1200
|
+
}
|
|
1201
|
+
seen.add(key)
|
|
1202
|
+
files.push(file)
|
|
1203
|
+
}
|
|
1204
|
+
try {
|
|
1205
|
+
Array.from(clipboardData.files || []).forEach(pushIfUnique)
|
|
1206
|
+
} catch (_) {}
|
|
1207
|
+
try {
|
|
1208
|
+
const items = clipboardData.items ? Array.from(clipboardData.items) : []
|
|
1209
|
+
items.forEach((item) => {
|
|
1210
|
+
if (!item || item.kind !== "file" || typeof item.getAsFile !== "function") {
|
|
1211
|
+
return
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
pushIfUnique(item.getAsFile())
|
|
1215
|
+
} catch (_) {}
|
|
1216
|
+
})
|
|
1217
|
+
} catch (_) {}
|
|
1218
|
+
return { files, hasFileFlavor }
|
|
1219
|
+
}
|
|
1220
|
+
const readClipboardFilesFallback = async () => {
|
|
1221
|
+
if (!navigator.clipboard || typeof navigator.clipboard.read !== "function") {
|
|
1222
|
+
return []
|
|
1223
|
+
}
|
|
1224
|
+
try {
|
|
1225
|
+
const clipboardItems = await navigator.clipboard.read()
|
|
1226
|
+
const collected = []
|
|
1227
|
+
const seen = new Set()
|
|
1228
|
+
let index = 0
|
|
1229
|
+
for (const item of clipboardItems) {
|
|
1230
|
+
if (!item || !Array.isArray(item.types)) {
|
|
1231
|
+
index += 1
|
|
1232
|
+
continue
|
|
1233
|
+
}
|
|
1234
|
+
const types = item.types
|
|
1235
|
+
const preferredType = types.find((type) => type && !type.startsWith("text/"))
|
|
1236
|
+
|| types.find((type) => /^image\//i.test(type))
|
|
1237
|
+
|| types[0]
|
|
1238
|
+
if (!preferredType) {
|
|
1239
|
+
index += 1
|
|
1240
|
+
continue
|
|
1241
|
+
}
|
|
1242
|
+
try {
|
|
1243
|
+
const blob = await item.getType(preferredType)
|
|
1244
|
+
if (!blob) {
|
|
1245
|
+
index += 1
|
|
1246
|
+
continue
|
|
1247
|
+
}
|
|
1248
|
+
let name = typeof item.name === "string" && item.name ? item.name : ""
|
|
1249
|
+
if (!name) {
|
|
1250
|
+
const ext = preferredType && preferredType.includes("/") ? preferredType.split("/").pop() : ""
|
|
1251
|
+
const safeExt = ext ? ext.replace(/[^a-z0-9]/gi, "").toLowerCase() : ""
|
|
1252
|
+
name = `clipboard-${Date.now()}-${index}${safeExt ? `.${safeExt}` : ""}`
|
|
1253
|
+
}
|
|
1254
|
+
const file = blob instanceof File ? blob : new File([blob], name, {
|
|
1255
|
+
type: blob.type || preferredType,
|
|
1256
|
+
lastModified: Date.now()
|
|
1257
|
+
})
|
|
1258
|
+
const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
|
|
1259
|
+
if (seen.has(key)) {
|
|
1260
|
+
index += 1
|
|
1261
|
+
continue
|
|
1262
|
+
}
|
|
1263
|
+
seen.add(key)
|
|
1264
|
+
collected.push(file)
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
if (error && error.name !== "NotAllowedError") {
|
|
1267
|
+
console.warn("Failed to extract clipboard blob", error)
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
index += 1
|
|
1271
|
+
}
|
|
1272
|
+
return collected
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
if (error && error.name !== "NotAllowedError") {
|
|
1275
|
+
console.warn("navigator.clipboard.read() failed", error)
|
|
1276
|
+
}
|
|
1277
|
+
return []
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const extractUrlsFromClipboard = (clipboardData) => {
|
|
1281
|
+
const urls = []
|
|
1282
|
+
if (!clipboardData) {
|
|
1283
|
+
return urls
|
|
1284
|
+
}
|
|
1285
|
+
const seen = new Set()
|
|
1286
|
+
const pushUrl = (href, nameHint) => {
|
|
1287
|
+
if (!href) {
|
|
1288
|
+
return
|
|
1289
|
+
}
|
|
1290
|
+
let resolved
|
|
1291
|
+
try {
|
|
1292
|
+
resolved = new URL(href, window.location.href)
|
|
1293
|
+
} catch (_) {
|
|
1294
|
+
try {
|
|
1295
|
+
resolved = new URL(href)
|
|
1296
|
+
} catch (_) {
|
|
1297
|
+
return
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (!/^https?:$/i.test(resolved.protocol)) {
|
|
1301
|
+
return
|
|
1302
|
+
}
|
|
1303
|
+
const key = resolved.href
|
|
1304
|
+
if (seen.has(key)) {
|
|
1305
|
+
return
|
|
1175
1306
|
}
|
|
1176
1307
|
seen.add(key)
|
|
1308
|
+
urls.push({ href: resolved.href, name: nameHint || null })
|
|
1309
|
+
}
|
|
1310
|
+
let html = ""
|
|
1311
|
+
let uriList = ""
|
|
1312
|
+
if (typeof clipboardData.getData === "function") {
|
|
1313
|
+
try { html = clipboardData.getData("text/html") || "" } catch (_) {}
|
|
1314
|
+
try { uriList = clipboardData.getData("text/uri-list") || "" } catch (_) {}
|
|
1315
|
+
}
|
|
1316
|
+
if (uriList) {
|
|
1317
|
+
uriList.split(/\r?\n/).forEach((line) => {
|
|
1318
|
+
const trimmed = line.trim()
|
|
1319
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1320
|
+
return
|
|
1321
|
+
}
|
|
1322
|
+
pushUrl(trimmed, null)
|
|
1323
|
+
})
|
|
1324
|
+
}
|
|
1325
|
+
if (html) {
|
|
1326
|
+
try {
|
|
1327
|
+
const parser = new DOMParser()
|
|
1328
|
+
const doc = parser.parseFromString(html, "text/html")
|
|
1329
|
+
doc.querySelectorAll("img[src]").forEach((img) => {
|
|
1330
|
+
const src = img.getAttribute("src")
|
|
1331
|
+
const nameHint = img.getAttribute("alt") || img.getAttribute("title") || null
|
|
1332
|
+
pushUrl(src, nameHint)
|
|
1333
|
+
})
|
|
1334
|
+
doc.querySelectorAll("a[href]").forEach((anchor) => {
|
|
1335
|
+
const href = anchor.getAttribute("href")
|
|
1336
|
+
const nameHint = anchor.getAttribute("download") || (anchor.textContent ? anchor.textContent.trim() : null)
|
|
1337
|
+
pushUrl(href, nameHint)
|
|
1338
|
+
})
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
console.warn("Failed to parse clipboard HTML", error)
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return urls
|
|
1344
|
+
}
|
|
1345
|
+
const isLikelyImagePaste = (clipboardData) => {
|
|
1346
|
+
if (!clipboardData) {
|
|
1347
|
+
return false
|
|
1348
|
+
}
|
|
1349
|
+
const types = Array.from(clipboardData.types || [])
|
|
1350
|
+
if (types.some((type) => type && type.toLowerCase().startsWith("image/"))) {
|
|
1177
1351
|
return true
|
|
1178
|
-
}
|
|
1352
|
+
}
|
|
1353
|
+
if (typeof clipboardData.getData === "function") {
|
|
1354
|
+
try {
|
|
1355
|
+
const html = clipboardData.getData("text/html") || ""
|
|
1356
|
+
if (html && /<img\b/i.test(html)) {
|
|
1357
|
+
return true
|
|
1358
|
+
}
|
|
1359
|
+
} catch (_) {}
|
|
1360
|
+
try {
|
|
1361
|
+
const uriList = clipboardData.getData("text/uri-list") || ""
|
|
1362
|
+
if (uriList && uriList.split(/\r?\n/).some((line) => {
|
|
1363
|
+
const trimmed = line.trim()
|
|
1364
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1365
|
+
return false
|
|
1366
|
+
}
|
|
1367
|
+
return /^https?:\/\//i.test(trimmed)
|
|
1368
|
+
})) {
|
|
1369
|
+
return true
|
|
1370
|
+
}
|
|
1371
|
+
} catch (_) {}
|
|
1372
|
+
}
|
|
1373
|
+
return false
|
|
1179
1374
|
}
|
|
1375
|
+
let suppressClipboardText = false
|
|
1180
1376
|
let dragDepth = 0
|
|
1181
1377
|
const prevent = (event) => {
|
|
1182
1378
|
event.preventDefault()
|
|
@@ -1203,7 +1399,20 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1203
1399
|
let remoteResources = []
|
|
1204
1400
|
try {
|
|
1205
1401
|
const extra = await this.collectFilesFromDataTransfer(event.dataTransfer)
|
|
1206
|
-
|
|
1402
|
+
if (extra && Array.isArray(extra.urls) && extra.urls.length) {
|
|
1403
|
+
const seenUrls = new Set()
|
|
1404
|
+
remoteResources = extra.urls.filter((item) => {
|
|
1405
|
+
if (!item || typeof item.href !== "string") {
|
|
1406
|
+
return false
|
|
1407
|
+
}
|
|
1408
|
+
const key = item.href.trim()
|
|
1409
|
+
if (!key || seenUrls.has(key)) {
|
|
1410
|
+
return false
|
|
1411
|
+
}
|
|
1412
|
+
seenUrls.add(key)
|
|
1413
|
+
return true
|
|
1414
|
+
})
|
|
1415
|
+
}
|
|
1207
1416
|
} catch (error) {
|
|
1208
1417
|
console.warn("Failed to collect files from drop payload", error)
|
|
1209
1418
|
}
|
|
@@ -1223,35 +1432,41 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1223
1432
|
this.term.focus()
|
|
1224
1433
|
})
|
|
1225
1434
|
terminalContainer.addEventListener("paste", async (event) => {
|
|
1226
|
-
const clipboard = event.clipboardData
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
} catch (error) {
|
|
1236
|
-
console.warn("Failed to collect clipboard resources", error)
|
|
1435
|
+
const clipboard = event && event.clipboardData ? event.clipboardData : null
|
|
1436
|
+
const { files: directFiles, hasFileFlavor } = collectClipboardFiles(clipboard)
|
|
1437
|
+
let files = directFiles
|
|
1438
|
+
let remoteUrls = []
|
|
1439
|
+
if (files.length === 0) {
|
|
1440
|
+
remoteUrls = extractUrlsFromClipboard(clipboard)
|
|
1441
|
+
if (remoteUrls.length === 0 && !hasFileFlavor && isLikelyImagePaste(clipboard)) {
|
|
1442
|
+
files = await readClipboardFilesFallback()
|
|
1443
|
+
}
|
|
1237
1444
|
}
|
|
1238
|
-
|
|
1445
|
+
console.log('[clipboard paste][shell]', {
|
|
1446
|
+
files: files.map((file) => ({ name: file.name, size: file.size, type: file.type })),
|
|
1447
|
+
remoteUrls,
|
|
1448
|
+
hasFileFlavor,
|
|
1449
|
+
types: clipboard ? Array.from(clipboard.types || []) : []
|
|
1450
|
+
})
|
|
1451
|
+
if (files.length === 0 && remoteUrls.length === 0) {
|
|
1452
|
+
suppressClipboardText = false
|
|
1239
1453
|
return
|
|
1240
1454
|
}
|
|
1241
1455
|
event.preventDefault()
|
|
1242
1456
|
event.stopPropagation()
|
|
1457
|
+
suppressClipboardText = true
|
|
1243
1458
|
try {
|
|
1244
|
-
if (
|
|
1245
|
-
await this.uploadRemoteResources(
|
|
1459
|
+
if (remoteUrls.length > 0) {
|
|
1460
|
+
await this.uploadRemoteResources(remoteUrls, dropOverlay)
|
|
1461
|
+
}
|
|
1462
|
+
if (files.length > 0) {
|
|
1463
|
+
await this.uploadFiles(files, dropOverlay)
|
|
1246
1464
|
}
|
|
1247
1465
|
} catch (error) {
|
|
1248
|
-
console.warn("Clipboard
|
|
1249
|
-
}
|
|
1250
|
-
if (files.length > 0) {
|
|
1251
|
-
await this.uploadFiles(files, dropOverlay)
|
|
1466
|
+
console.warn("Clipboard upload failed", error)
|
|
1252
1467
|
}
|
|
1253
1468
|
this.term.focus()
|
|
1254
|
-
})
|
|
1469
|
+
}, true)
|
|
1255
1470
|
term.attachCustomKeyEventHandler(event => {
|
|
1256
1471
|
if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
|
|
1257
1472
|
const selection = term.getSelection();
|
|
@@ -1261,16 +1476,20 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1261
1476
|
}
|
|
1262
1477
|
}
|
|
1263
1478
|
if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1479
|
+
if (!suppressClipboardText) {
|
|
1480
|
+
navigator.clipboard.readText().then((text) => {
|
|
1481
|
+
console.log('[clipboard paste][shell][text]', text)
|
|
1482
|
+
this.socket.run({
|
|
1483
|
+
//key: "\x1b[200~" + text + "\x1b[201~",
|
|
1484
|
+
key: text,
|
|
1485
|
+
id: shell_id,
|
|
1486
|
+
paste: true
|
|
1487
|
+
})
|
|
1488
|
+
this.captureTextInput(text)
|
|
1272
1489
|
|
|
1273
|
-
|
|
1490
|
+
})
|
|
1491
|
+
}
|
|
1492
|
+
suppressClipboardText = false
|
|
1274
1493
|
return false
|
|
1275
1494
|
}
|
|
1276
1495
|
return true;
|
|
@@ -767,6 +767,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
767
767
|
} else if (packet.type === "result") {
|
|
768
768
|
if (packet.id === "terminal.upload") {
|
|
769
769
|
const uploaded = Array.isArray(packet.data && packet.data.files) ? packet.data.files : []
|
|
770
|
+
console.log('[terminal.upload][terminal][result]', uploaded)
|
|
770
771
|
if (uploaded.length > 0) {
|
|
771
772
|
const mappedFiles = uploaded.map((file) => {
|
|
772
773
|
const displayPath = file.displayPath || file.homeRelativePath ? `~/${(file.homeRelativePath || '').replace(/^\/+/, '')}` : (file.path || '')
|
|
@@ -1256,99 +1257,252 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1256
1257
|
dropOverlay.className = "terminal-drop-overlay"
|
|
1257
1258
|
dropOverlay.textContent = "Drop files to upload"
|
|
1258
1259
|
terminalContainer.appendChild(dropOverlay)
|
|
1259
|
-
const
|
|
1260
|
+
const dedupeClipboardFiles = (inputs) => {
|
|
1260
1261
|
const seen = new Set()
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1262
|
+
const results = []
|
|
1263
|
+
inputs.forEach((file) => {
|
|
1264
|
+
if (!file || !(file instanceof File) || !(file.size > 0)) {
|
|
1265
|
+
return
|
|
1264
1266
|
}
|
|
1265
|
-
const
|
|
1266
|
-
if (
|
|
1267
|
-
return
|
|
1267
|
+
const signature = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
|
|
1268
|
+
if (seen.has(signature)) {
|
|
1269
|
+
return
|
|
1268
1270
|
}
|
|
1269
|
-
seen.add(
|
|
1270
|
-
|
|
1271
|
+
seen.add(signature)
|
|
1272
|
+
results.push(file)
|
|
1271
1273
|
})
|
|
1274
|
+
return results
|
|
1272
1275
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
terminalContainer.addEventListener("dragenter", (event) => {
|
|
1279
|
-
prevent(event)
|
|
1280
|
-
dragDepth += 1
|
|
1281
|
-
dropOverlay.classList.add("active")
|
|
1282
|
-
})
|
|
1283
|
-
terminalContainer.addEventListener("dragover", prevent)
|
|
1284
|
-
terminalContainer.addEventListener("dragleave", (event) => {
|
|
1285
|
-
prevent(event)
|
|
1286
|
-
dragDepth = Math.max(0, dragDepth - 1)
|
|
1287
|
-
if (dragDepth === 0) {
|
|
1288
|
-
dropOverlay.classList.remove("active")
|
|
1276
|
+
const collectClipboardFiles = (clipboardData) => {
|
|
1277
|
+
const files = []
|
|
1278
|
+
const seen = new Set()
|
|
1279
|
+
if (!clipboardData) {
|
|
1280
|
+
return { files, hasFileFlavor: false }
|
|
1289
1281
|
}
|
|
1290
|
-
|
|
1291
|
-
terminalContainer.addEventListener("drop", async (event) => {
|
|
1292
|
-
prevent(event)
|
|
1293
|
-
dragDepth = 0
|
|
1294
|
-
dropOverlay.classList.remove("active")
|
|
1295
|
-
const files = Array.from(event.dataTransfer ? event.dataTransfer.files || [] : [])
|
|
1296
|
-
let remoteResources = []
|
|
1282
|
+
let hasFileFlavor = false
|
|
1297
1283
|
try {
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
} catch (
|
|
1301
|
-
|
|
1284
|
+
const types = Array.from(clipboardData.types || [])
|
|
1285
|
+
hasFileFlavor = types.some((type) => type === "Files" || type === "application/x-moz-file")
|
|
1286
|
+
} catch (_) {}
|
|
1287
|
+
const pushIfUnique = (file) => {
|
|
1288
|
+
if (!file || !(file instanceof File) || !(file.size > 0)) {
|
|
1289
|
+
return
|
|
1290
|
+
}
|
|
1291
|
+
const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
|
|
1292
|
+
if (seen.has(key)) {
|
|
1293
|
+
return
|
|
1294
|
+
}
|
|
1295
|
+
seen.add(key)
|
|
1296
|
+
files.push(file)
|
|
1302
1297
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1298
|
+
try {
|
|
1299
|
+
Array.from(clipboardData.files || []).forEach(pushIfUnique)
|
|
1300
|
+
} catch (_) {}
|
|
1301
|
+
try {
|
|
1302
|
+
const items = clipboardData.items ? Array.from(clipboardData.items) : []
|
|
1303
|
+
items.forEach((item) => {
|
|
1304
|
+
if (!item || item.kind !== "file" || typeof item.getAsFile !== "function") {
|
|
1305
|
+
return
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
pushIfUnique(item.getAsFile())
|
|
1309
|
+
} catch (_) {}
|
|
1307
1310
|
})
|
|
1308
|
-
|
|
1311
|
+
} catch (_) {}
|
|
1312
|
+
return { files, hasFileFlavor }
|
|
1313
|
+
}
|
|
1314
|
+
const readClipboardFilesFallback = async () => {
|
|
1315
|
+
if (!navigator.clipboard || typeof navigator.clipboard.read !== "function") {
|
|
1316
|
+
return []
|
|
1309
1317
|
}
|
|
1310
1318
|
try {
|
|
1311
|
-
|
|
1312
|
-
|
|
1319
|
+
const clipboardItems = await navigator.clipboard.read()
|
|
1320
|
+
const collected = []
|
|
1321
|
+
const seen = new Set()
|
|
1322
|
+
let index = 0
|
|
1323
|
+
for (const item of clipboardItems) {
|
|
1324
|
+
if (!item || !Array.isArray(item.types)) {
|
|
1325
|
+
index += 1
|
|
1326
|
+
continue
|
|
1327
|
+
}
|
|
1328
|
+
const types = item.types
|
|
1329
|
+
const preferredType = types.find((type) => type && !type.startsWith("text/"))
|
|
1330
|
+
|| types.find((type) => /^image\//i.test(type))
|
|
1331
|
+
|| types[0]
|
|
1332
|
+
if (!preferredType) {
|
|
1333
|
+
index += 1
|
|
1334
|
+
continue
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
const blob = await item.getType(preferredType)
|
|
1338
|
+
if (!blob) {
|
|
1339
|
+
index += 1
|
|
1340
|
+
continue
|
|
1341
|
+
}
|
|
1342
|
+
let name = typeof item.name === "string" && item.name ? item.name : ""
|
|
1343
|
+
if (!name) {
|
|
1344
|
+
const ext = preferredType && preferredType.includes("/") ? preferredType.split("/").pop() : ""
|
|
1345
|
+
const safeExt = ext ? ext.replace(/[^a-z0-9]/gi, "").toLowerCase() : ""
|
|
1346
|
+
name = `clipboard-${Date.now()}-${index}${safeExt ? `.${safeExt}` : ""}`
|
|
1347
|
+
}
|
|
1348
|
+
const file = blob instanceof File ? blob : new File([blob], name, {
|
|
1349
|
+
type: blob.type || preferredType,
|
|
1350
|
+
lastModified: Date.now()
|
|
1351
|
+
})
|
|
1352
|
+
const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
|
|
1353
|
+
if (seen.has(key)) {
|
|
1354
|
+
index += 1
|
|
1355
|
+
continue
|
|
1356
|
+
}
|
|
1357
|
+
seen.add(key)
|
|
1358
|
+
collected.push(file)
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
if (error && error.name !== "NotAllowedError") {
|
|
1361
|
+
console.warn("Failed to extract clipboard blob", error)
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
index += 1
|
|
1313
1365
|
}
|
|
1366
|
+
return collected
|
|
1314
1367
|
} catch (error) {
|
|
1315
|
-
|
|
1368
|
+
if (error && error.name !== "NotAllowedError") {
|
|
1369
|
+
console.warn("navigator.clipboard.read() failed", error)
|
|
1370
|
+
}
|
|
1371
|
+
return []
|
|
1316
1372
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1373
|
+
}
|
|
1374
|
+
const extractUrlsFromClipboard = (clipboardData) => {
|
|
1375
|
+
const urls = []
|
|
1376
|
+
if (!clipboardData) {
|
|
1377
|
+
return urls
|
|
1319
1378
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1379
|
+
const seen = new Set()
|
|
1380
|
+
const pushUrl = (href, nameHint) => {
|
|
1381
|
+
if (!href) {
|
|
1382
|
+
return
|
|
1383
|
+
}
|
|
1384
|
+
let resolved
|
|
1385
|
+
try {
|
|
1386
|
+
resolved = new URL(href, window.location.href)
|
|
1387
|
+
} catch (_) {
|
|
1388
|
+
try {
|
|
1389
|
+
resolved = new URL(href)
|
|
1390
|
+
} catch (_) {
|
|
1391
|
+
return
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (!/^https?:$/i.test(resolved.protocol)) {
|
|
1395
|
+
return
|
|
1396
|
+
}
|
|
1397
|
+
const key = resolved.href
|
|
1398
|
+
if (seen.has(key)) {
|
|
1399
|
+
return
|
|
1400
|
+
}
|
|
1401
|
+
seen.add(key)
|
|
1402
|
+
urls.push({ href: resolved.href, name: nameHint || null })
|
|
1326
1403
|
}
|
|
1327
|
-
|
|
1328
|
-
let
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1404
|
+
let html = ""
|
|
1405
|
+
let uriList = ""
|
|
1406
|
+
if (typeof clipboardData.getData === "function") {
|
|
1407
|
+
try { html = clipboardData.getData("text/html") || "" } catch (_) {}
|
|
1408
|
+
try { uriList = clipboardData.getData("text/uri-list") || "" } catch (_) {}
|
|
1409
|
+
}
|
|
1410
|
+
if (uriList) {
|
|
1411
|
+
uriList.split(/\r?\n/).forEach((line) => {
|
|
1412
|
+
const trimmed = line.trim()
|
|
1413
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1414
|
+
return
|
|
1415
|
+
}
|
|
1416
|
+
pushUrl(trimmed, null)
|
|
1417
|
+
})
|
|
1418
|
+
}
|
|
1419
|
+
if (html) {
|
|
1420
|
+
try {
|
|
1421
|
+
const parser = new DOMParser()
|
|
1422
|
+
const doc = parser.parseFromString(html, "text/html")
|
|
1423
|
+
doc.querySelectorAll("img[src]").forEach((img) => {
|
|
1424
|
+
const src = img.getAttribute("src")
|
|
1425
|
+
const nameHint = img.getAttribute("alt") || img.getAttribute("title") || null
|
|
1426
|
+
pushUrl(src, nameHint)
|
|
1427
|
+
})
|
|
1428
|
+
doc.querySelectorAll("a[href]").forEach((anchor) => {
|
|
1429
|
+
const href = anchor.getAttribute("href")
|
|
1430
|
+
const nameHint = anchor.getAttribute("download") || (anchor.textContent ? anchor.textContent.trim() : null)
|
|
1431
|
+
pushUrl(href, nameHint)
|
|
1432
|
+
})
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
console.warn("Failed to parse clipboard HTML", error)
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return urls
|
|
1438
|
+
}
|
|
1439
|
+
const isLikelyImagePaste = (clipboardData) => {
|
|
1440
|
+
if (!clipboardData) {
|
|
1441
|
+
return false
|
|
1442
|
+
}
|
|
1443
|
+
const types = Array.from(clipboardData.types || [])
|
|
1444
|
+
if (types.some((type) => type && type.toLowerCase().startsWith("image/"))) {
|
|
1445
|
+
return true
|
|
1446
|
+
}
|
|
1447
|
+
if (typeof clipboardData.getData === "function") {
|
|
1448
|
+
try {
|
|
1449
|
+
const html = clipboardData.getData("text/html") || ""
|
|
1450
|
+
if (html && /<img\b/i.test(html)) {
|
|
1451
|
+
return true
|
|
1452
|
+
}
|
|
1453
|
+
} catch (_) {}
|
|
1454
|
+
try {
|
|
1455
|
+
const uriList = clipboardData.getData("text/uri-list") || ""
|
|
1456
|
+
if (uriList && uriList.split(/\r?\n/).some((line) => {
|
|
1457
|
+
const trimmed = line.trim()
|
|
1458
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1459
|
+
return false
|
|
1460
|
+
}
|
|
1461
|
+
return /^https?:\/\//i.test(trimmed)
|
|
1462
|
+
})) {
|
|
1463
|
+
return true
|
|
1464
|
+
}
|
|
1465
|
+
} catch (_) {}
|
|
1466
|
+
}
|
|
1467
|
+
return false
|
|
1468
|
+
}
|
|
1469
|
+
let suppressClipboardText = false
|
|
1470
|
+
terminalContainer.addEventListener("paste", async (event) => {
|
|
1471
|
+
const clipboard = event && event.clipboardData ? event.clipboardData : null
|
|
1472
|
+
const { files: directFiles, hasFileFlavor } = collectClipboardFiles(clipboard)
|
|
1473
|
+
let files = directFiles
|
|
1474
|
+
let remoteUrls = []
|
|
1475
|
+
if (files.length === 0) {
|
|
1476
|
+
remoteUrls = extractUrlsFromClipboard(clipboard)
|
|
1477
|
+
if (remoteUrls.length === 0 && !hasFileFlavor && isLikelyImagePaste(clipboard)) {
|
|
1478
|
+
files = await readClipboardFilesFallback()
|
|
1479
|
+
}
|
|
1334
1480
|
}
|
|
1335
|
-
|
|
1481
|
+
console.log('[clipboard paste][terminal]', {
|
|
1482
|
+
files: files.map((file) => ({ name: file.name, size: file.size, type: file.type })),
|
|
1483
|
+
remoteUrls,
|
|
1484
|
+
hasFileFlavor,
|
|
1485
|
+
types: clipboard ? Array.from(clipboard.types || []) : []
|
|
1486
|
+
})
|
|
1487
|
+
if (files.length === 0 && remoteUrls.length === 0) {
|
|
1488
|
+
suppressClipboardText = false
|
|
1336
1489
|
return
|
|
1337
1490
|
}
|
|
1338
1491
|
event.preventDefault()
|
|
1339
1492
|
event.stopPropagation()
|
|
1493
|
+
suppressClipboardText = true
|
|
1340
1494
|
try {
|
|
1341
|
-
if (
|
|
1342
|
-
await this.uploadRemoteResources(
|
|
1495
|
+
if (remoteUrls.length > 0) {
|
|
1496
|
+
await this.uploadRemoteResources(remoteUrls, dropOverlay)
|
|
1497
|
+
}
|
|
1498
|
+
if (files.length > 0) {
|
|
1499
|
+
await this.uploadFiles(files, dropOverlay)
|
|
1343
1500
|
}
|
|
1344
1501
|
} catch (error) {
|
|
1345
|
-
console.warn("Clipboard
|
|
1346
|
-
}
|
|
1347
|
-
if (files.length > 0) {
|
|
1348
|
-
await this.uploadFiles(files, dropOverlay)
|
|
1502
|
+
console.warn("Clipboard upload failed", error)
|
|
1349
1503
|
}
|
|
1350
1504
|
this.term.focus()
|
|
1351
|
-
})
|
|
1505
|
+
}, true)
|
|
1352
1506
|
term.attachCustomKeyEventHandler(event => {
|
|
1353
1507
|
if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
|
|
1354
1508
|
const selection = term.getSelection();
|
|
@@ -1358,14 +1512,16 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1358
1512
|
}
|
|
1359
1513
|
}
|
|
1360
1514
|
if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1515
|
+
if (!suppressClipboardText) {
|
|
1516
|
+
navigator.clipboard.readText().then((text) => {
|
|
1517
|
+
console.log('[clipboard paste][terminal][text]', text)
|
|
1518
|
+
this.socket.run({
|
|
1519
|
+
//key: "\x1b[200~" + text + "\x1b[201~",
|
|
1520
|
+
key: text,
|
|
1521
|
+
id: shell_id,
|
|
1522
|
+
paste: true
|
|
1523
|
+
})
|
|
1524
|
+
this.captureTextInput(text)
|
|
1369
1525
|
|
|
1370
1526
|
|
|
1371
1527
|
// this.socket.run({
|
|
@@ -1373,7 +1529,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1373
1529
|
// key: text,
|
|
1374
1530
|
// id: shell_id
|
|
1375
1531
|
// })
|
|
1376
|
-
|
|
1532
|
+
})
|
|
1533
|
+
}
|
|
1534
|
+
suppressClipboardText = false
|
|
1377
1535
|
return false
|
|
1378
1536
|
}
|
|
1379
1537
|
return true;
|