pinokiod 3.194.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 +140 -57
- package/server/views/terminal.ejs +134 -112
package/package.json
CHANGED
package/server/views/shell.ejs
CHANGED
|
@@ -1181,20 +1181,28 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1181
1181
|
}
|
|
1182
1182
|
const collectClipboardFiles = (clipboardData) => {
|
|
1183
1183
|
const files = []
|
|
1184
|
-
|
|
1184
|
+
const seen = new Set()
|
|
1185
1185
|
if (!clipboardData) {
|
|
1186
|
-
return { files, hasFileFlavor }
|
|
1186
|
+
return { files, hasFileFlavor: false }
|
|
1187
1187
|
}
|
|
1188
|
+
let hasFileFlavor = false
|
|
1188
1189
|
try {
|
|
1189
|
-
const types =
|
|
1190
|
+
const types = Array.from(clipboardData.types || [])
|
|
1190
1191
|
hasFileFlavor = types.some((type) => type === "Files" || type === "application/x-moz-file")
|
|
1191
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
|
+
}
|
|
1192
1204
|
try {
|
|
1193
|
-
Array.from(clipboardData.files || []).forEach(
|
|
1194
|
-
if (file && file.size > 0) {
|
|
1195
|
-
files.push(file)
|
|
1196
|
-
}
|
|
1197
|
-
})
|
|
1205
|
+
Array.from(clipboardData.files || []).forEach(pushIfUnique)
|
|
1198
1206
|
} catch (_) {}
|
|
1199
1207
|
try {
|
|
1200
1208
|
const items = clipboardData.items ? Array.from(clipboardData.items) : []
|
|
@@ -1203,14 +1211,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1203
1211
|
return
|
|
1204
1212
|
}
|
|
1205
1213
|
try {
|
|
1206
|
-
|
|
1207
|
-
if (file && file.size > 0) {
|
|
1208
|
-
files.push(file)
|
|
1209
|
-
}
|
|
1214
|
+
pushIfUnique(item.getAsFile())
|
|
1210
1215
|
} catch (_) {}
|
|
1211
1216
|
})
|
|
1212
1217
|
} catch (_) {}
|
|
1213
|
-
return { files
|
|
1218
|
+
return { files, hasFileFlavor }
|
|
1214
1219
|
}
|
|
1215
1220
|
const readClipboardFilesFallback = async () => {
|
|
1216
1221
|
if (!navigator.clipboard || typeof navigator.clipboard.read !== "function") {
|
|
@@ -1219,6 +1224,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1219
1224
|
try {
|
|
1220
1225
|
const clipboardItems = await navigator.clipboard.read()
|
|
1221
1226
|
const collected = []
|
|
1227
|
+
const seen = new Set()
|
|
1222
1228
|
let index = 0
|
|
1223
1229
|
for (const item of clipboardItems) {
|
|
1224
1230
|
if (!item || !Array.isArray(item.types)) {
|
|
@@ -1249,9 +1255,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1249
1255
|
type: blob.type || preferredType,
|
|
1250
1256
|
lastModified: Date.now()
|
|
1251
1257
|
})
|
|
1252
|
-
|
|
1253
|
-
|
|
1258
|
+
const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
|
|
1259
|
+
if (seen.has(key)) {
|
|
1260
|
+
index += 1
|
|
1261
|
+
continue
|
|
1254
1262
|
}
|
|
1263
|
+
seen.add(key)
|
|
1264
|
+
collected.push(file)
|
|
1255
1265
|
} catch (error) {
|
|
1256
1266
|
if (error && error.name !== "NotAllowedError") {
|
|
1257
1267
|
console.warn("Failed to extract clipboard blob", error)
|
|
@@ -1259,7 +1269,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1259
1269
|
}
|
|
1260
1270
|
index += 1
|
|
1261
1271
|
}
|
|
1262
|
-
return
|
|
1272
|
+
return collected
|
|
1263
1273
|
} catch (error) {
|
|
1264
1274
|
if (error && error.name !== "NotAllowedError") {
|
|
1265
1275
|
console.warn("navigator.clipboard.read() failed", error)
|
|
@@ -1267,6 +1277,101 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1267
1277
|
return []
|
|
1268
1278
|
}
|
|
1269
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
|
|
1306
|
+
}
|
|
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/"))) {
|
|
1351
|
+
return true
|
|
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
|
|
1374
|
+
}
|
|
1270
1375
|
let suppressClipboardText = false
|
|
1271
1376
|
let dragDepth = 0
|
|
1272
1377
|
const prevent = (event) => {
|
|
@@ -1328,43 +1433,22 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1328
1433
|
})
|
|
1329
1434
|
terminalContainer.addEventListener("paste", async (event) => {
|
|
1330
1435
|
const clipboard = event && event.clipboardData ? event.clipboardData : null
|
|
1331
|
-
const clipboardTypes = clipboard ? Array.from(clipboard.types || []) : []
|
|
1332
|
-
const hasFileLikeType = clipboardTypes.some((type) => {
|
|
1333
|
-
if (!type) {
|
|
1334
|
-
return false
|
|
1335
|
-
}
|
|
1336
|
-
const lower = type.toLowerCase()
|
|
1337
|
-
return lower === "text/uri-list" || lower === "text/html" || lower === "downloadurl" || lower === "text/x-moz-url" || lower.startsWith("image/")
|
|
1338
|
-
})
|
|
1339
1436
|
const { files: directFiles, hasFileFlavor } = collectClipboardFiles(clipboard)
|
|
1340
1437
|
let files = directFiles
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
if (files.length === 0 && allowUrlCollection) {
|
|
1347
|
-
try {
|
|
1348
|
-
const extra = await this.collectFilesFromDataTransfer(clipboard)
|
|
1349
|
-
if (extra && Array.isArray(extra.urls) && extra.urls.length) {
|
|
1350
|
-
const seen = new Set()
|
|
1351
|
-
remoteResources = extra.urls.filter((item) => {
|
|
1352
|
-
if (!item || typeof item.href !== "string") {
|
|
1353
|
-
return false
|
|
1354
|
-
}
|
|
1355
|
-
const key = item.href.trim()
|
|
1356
|
-
if (!key || seen.has(key)) {
|
|
1357
|
-
return false
|
|
1358
|
-
}
|
|
1359
|
-
seen.add(key)
|
|
1360
|
-
return true
|
|
1361
|
-
})
|
|
1362
|
-
}
|
|
1363
|
-
} catch (error) {
|
|
1364
|
-
console.warn("Failed to collect clipboard resources", error)
|
|
1438
|
+
let remoteUrls = []
|
|
1439
|
+
if (files.length === 0) {
|
|
1440
|
+
remoteUrls = extractUrlsFromClipboard(clipboard)
|
|
1441
|
+
if (remoteUrls.length === 0 && !hasFileFlavor && isLikelyImagePaste(clipboard)) {
|
|
1442
|
+
files = await readClipboardFilesFallback()
|
|
1365
1443
|
}
|
|
1366
1444
|
}
|
|
1367
|
-
|
|
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) {
|
|
1368
1452
|
suppressClipboardText = false
|
|
1369
1453
|
return
|
|
1370
1454
|
}
|
|
@@ -1372,16 +1456,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1372
1456
|
event.stopPropagation()
|
|
1373
1457
|
suppressClipboardText = true
|
|
1374
1458
|
try {
|
|
1375
|
-
if (
|
|
1376
|
-
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)
|
|
1377
1464
|
}
|
|
1378
1465
|
} catch (error) {
|
|
1379
|
-
console.warn("Clipboard
|
|
1380
|
-
}
|
|
1381
|
-
if (files.length > 0) {
|
|
1382
|
-
await this.uploadFiles(files, dropOverlay).catch((error) => {
|
|
1383
|
-
console.warn("Clipboard upload failed", error)
|
|
1384
|
-
})
|
|
1466
|
+
console.warn("Clipboard upload failed", error)
|
|
1385
1467
|
}
|
|
1386
1468
|
this.term.focus()
|
|
1387
1469
|
}, true)
|
|
@@ -1396,6 +1478,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1396
1478
|
if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
|
1397
1479
|
if (!suppressClipboardText) {
|
|
1398
1480
|
navigator.clipboard.readText().then((text) => {
|
|
1481
|
+
console.log('[clipboard paste][shell][text]', text)
|
|
1399
1482
|
this.socket.run({
|
|
1400
1483
|
//key: "\x1b[200~" + text + "\x1b[201~",
|
|
1401
1484
|
key: text,
|
|
@@ -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 || '')
|
|
@@ -1274,20 +1275,28 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1274
1275
|
}
|
|
1275
1276
|
const collectClipboardFiles = (clipboardData) => {
|
|
1276
1277
|
const files = []
|
|
1277
|
-
|
|
1278
|
+
const seen = new Set()
|
|
1278
1279
|
if (!clipboardData) {
|
|
1279
|
-
return { files, hasFileFlavor }
|
|
1280
|
+
return { files, hasFileFlavor: false }
|
|
1280
1281
|
}
|
|
1282
|
+
let hasFileFlavor = false
|
|
1281
1283
|
try {
|
|
1282
|
-
const types =
|
|
1284
|
+
const types = Array.from(clipboardData.types || [])
|
|
1283
1285
|
hasFileFlavor = types.some((type) => type === "Files" || type === "application/x-moz-file")
|
|
1284
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)
|
|
1297
|
+
}
|
|
1285
1298
|
try {
|
|
1286
|
-
Array.from(clipboardData.files || []).forEach(
|
|
1287
|
-
if (file && file.size > 0) {
|
|
1288
|
-
files.push(file)
|
|
1289
|
-
}
|
|
1290
|
-
})
|
|
1299
|
+
Array.from(clipboardData.files || []).forEach(pushIfUnique)
|
|
1291
1300
|
} catch (_) {}
|
|
1292
1301
|
try {
|
|
1293
1302
|
const items = clipboardData.items ? Array.from(clipboardData.items) : []
|
|
@@ -1296,14 +1305,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1296
1305
|
return
|
|
1297
1306
|
}
|
|
1298
1307
|
try {
|
|
1299
|
-
|
|
1300
|
-
if (file && file.size > 0) {
|
|
1301
|
-
files.push(file)
|
|
1302
|
-
}
|
|
1308
|
+
pushIfUnique(item.getAsFile())
|
|
1303
1309
|
} catch (_) {}
|
|
1304
1310
|
})
|
|
1305
1311
|
} catch (_) {}
|
|
1306
|
-
return { files
|
|
1312
|
+
return { files, hasFileFlavor }
|
|
1307
1313
|
}
|
|
1308
1314
|
const readClipboardFilesFallback = async () => {
|
|
1309
1315
|
if (!navigator.clipboard || typeof navigator.clipboard.read !== "function") {
|
|
@@ -1312,6 +1318,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1312
1318
|
try {
|
|
1313
1319
|
const clipboardItems = await navigator.clipboard.read()
|
|
1314
1320
|
const collected = []
|
|
1321
|
+
const seen = new Set()
|
|
1315
1322
|
let index = 0
|
|
1316
1323
|
for (const item of clipboardItems) {
|
|
1317
1324
|
if (!item || !Array.isArray(item.types)) {
|
|
@@ -1342,9 +1349,13 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1342
1349
|
type: blob.type || preferredType,
|
|
1343
1350
|
lastModified: Date.now()
|
|
1344
1351
|
})
|
|
1345
|
-
|
|
1346
|
-
|
|
1352
|
+
const key = `${file.name || ''}::${file.size || 0}::${file.type || ''}`
|
|
1353
|
+
if (seen.has(key)) {
|
|
1354
|
+
index += 1
|
|
1355
|
+
continue
|
|
1347
1356
|
}
|
|
1357
|
+
seen.add(key)
|
|
1358
|
+
collected.push(file)
|
|
1348
1359
|
} catch (error) {
|
|
1349
1360
|
if (error && error.name !== "NotAllowedError") {
|
|
1350
1361
|
console.warn("Failed to extract clipboard blob", error)
|
|
@@ -1352,7 +1363,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1352
1363
|
}
|
|
1353
1364
|
index += 1
|
|
1354
1365
|
}
|
|
1355
|
-
return
|
|
1366
|
+
return collected
|
|
1356
1367
|
} catch (error) {
|
|
1357
1368
|
if (error && error.name !== "NotAllowedError") {
|
|
1358
1369
|
console.warn("navigator.clipboard.read() failed", error)
|
|
@@ -1360,108 +1371,120 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1360
1371
|
return []
|
|
1361
1372
|
}
|
|
1362
1373
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
event.stopPropagation()
|
|
1368
|
-
}
|
|
1369
|
-
terminalContainer.addEventListener("dragenter", (event) => {
|
|
1370
|
-
prevent(event)
|
|
1371
|
-
dragDepth += 1
|
|
1372
|
-
dropOverlay.classList.add("active")
|
|
1373
|
-
})
|
|
1374
|
-
terminalContainer.addEventListener("dragover", prevent)
|
|
1375
|
-
terminalContainer.addEventListener("dragleave", (event) => {
|
|
1376
|
-
prevent(event)
|
|
1377
|
-
dragDepth = Math.max(0, dragDepth - 1)
|
|
1378
|
-
if (dragDepth === 0) {
|
|
1379
|
-
dropOverlay.classList.remove("active")
|
|
1374
|
+
const extractUrlsFromClipboard = (clipboardData) => {
|
|
1375
|
+
const urls = []
|
|
1376
|
+
if (!clipboardData) {
|
|
1377
|
+
return urls
|
|
1380
1378
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
dropOverlay.classList.remove("active")
|
|
1386
|
-
const files = Array.from(event.dataTransfer ? event.dataTransfer.files || [] : [])
|
|
1387
|
-
let remoteResources = []
|
|
1388
|
-
try {
|
|
1389
|
-
const extra = await this.collectFilesFromDataTransfer(event.dataTransfer)
|
|
1390
|
-
if (extra && Array.isArray(extra.urls) && extra.urls.length) {
|
|
1391
|
-
const seenUrls = new Set()
|
|
1392
|
-
remoteResources = extra.urls.filter((item) => {
|
|
1393
|
-
if (!item || typeof item.href !== "string") {
|
|
1394
|
-
return false
|
|
1395
|
-
}
|
|
1396
|
-
const key = item.href.trim()
|
|
1397
|
-
if (!key || seenUrls.has(key)) {
|
|
1398
|
-
return false
|
|
1399
|
-
}
|
|
1400
|
-
seenUrls.add(key)
|
|
1401
|
-
return true
|
|
1402
|
-
})
|
|
1379
|
+
const seen = new Set()
|
|
1380
|
+
const pushUrl = (href, nameHint) => {
|
|
1381
|
+
if (!href) {
|
|
1382
|
+
return
|
|
1403
1383
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
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 })
|
|
1406
1403
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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)
|
|
1411
1417
|
})
|
|
1412
|
-
return
|
|
1413
1418
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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)
|
|
1417
1435
|
}
|
|
1418
|
-
} catch (error) {
|
|
1419
|
-
console.warn("Remote upload failed", error)
|
|
1420
1436
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1437
|
+
return urls
|
|
1438
|
+
}
|
|
1439
|
+
const isLikelyImagePaste = (clipboardData) => {
|
|
1440
|
+
if (!clipboardData) {
|
|
1441
|
+
return false
|
|
1423
1442
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
|
1426
1470
|
terminalContainer.addEventListener("paste", async (event) => {
|
|
1427
1471
|
const clipboard = event && event.clipboardData ? event.clipboardData : null
|
|
1428
|
-
const clipboardTypes = clipboard ? Array.from(clipboard.types || []) : []
|
|
1429
|
-
const hasFileLikeType = clipboardTypes.some((type) => {
|
|
1430
|
-
if (!type) {
|
|
1431
|
-
return false
|
|
1432
|
-
}
|
|
1433
|
-
const lower = type.toLowerCase()
|
|
1434
|
-
return lower === "text/uri-list" || lower === "text/html" || lower === "downloadurl" || lower === "text/x-moz-url" || lower.startsWith("image/")
|
|
1435
|
-
})
|
|
1436
1472
|
const { files: directFiles, hasFileFlavor } = collectClipboardFiles(clipboard)
|
|
1437
1473
|
let files = directFiles
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
if (files.length === 0 && allowUrlCollection) {
|
|
1444
|
-
try {
|
|
1445
|
-
const extra = await this.collectFilesFromDataTransfer(clipboard)
|
|
1446
|
-
if (extra && Array.isArray(extra.urls) && extra.urls.length) {
|
|
1447
|
-
const seen = new Set()
|
|
1448
|
-
remoteResources = extra.urls.filter((item) => {
|
|
1449
|
-
if (!item || typeof item.href !== "string") {
|
|
1450
|
-
return false
|
|
1451
|
-
}
|
|
1452
|
-
const key = item.href.trim()
|
|
1453
|
-
if (!key || seen.has(key)) {
|
|
1454
|
-
return false
|
|
1455
|
-
}
|
|
1456
|
-
seen.add(key)
|
|
1457
|
-
return true
|
|
1458
|
-
})
|
|
1459
|
-
}
|
|
1460
|
-
} catch (error) {
|
|
1461
|
-
console.warn("Failed to collect clipboard resources", error)
|
|
1474
|
+
let remoteUrls = []
|
|
1475
|
+
if (files.length === 0) {
|
|
1476
|
+
remoteUrls = extractUrlsFromClipboard(clipboard)
|
|
1477
|
+
if (remoteUrls.length === 0 && !hasFileFlavor && isLikelyImagePaste(clipboard)) {
|
|
1478
|
+
files = await readClipboardFilesFallback()
|
|
1462
1479
|
}
|
|
1463
1480
|
}
|
|
1464
|
-
|
|
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) {
|
|
1465
1488
|
suppressClipboardText = false
|
|
1466
1489
|
return
|
|
1467
1490
|
}
|
|
@@ -1469,16 +1492,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1469
1492
|
event.stopPropagation()
|
|
1470
1493
|
suppressClipboardText = true
|
|
1471
1494
|
try {
|
|
1472
|
-
if (
|
|
1473
|
-
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)
|
|
1474
1500
|
}
|
|
1475
1501
|
} catch (error) {
|
|
1476
|
-
console.warn("Clipboard
|
|
1477
|
-
}
|
|
1478
|
-
if (files.length > 0) {
|
|
1479
|
-
await this.uploadFiles(files, dropOverlay).catch((error) => {
|
|
1480
|
-
console.warn("Clipboard upload failed", error)
|
|
1481
|
-
})
|
|
1502
|
+
console.warn("Clipboard upload failed", error)
|
|
1482
1503
|
}
|
|
1483
1504
|
this.term.focus()
|
|
1484
1505
|
}, true)
|
|
@@ -1493,6 +1514,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1493
1514
|
if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
|
1494
1515
|
if (!suppressClipboardText) {
|
|
1495
1516
|
navigator.clipboard.readText().then((text) => {
|
|
1517
|
+
console.log('[clipboard paste][terminal][text]', text)
|
|
1496
1518
|
this.socket.run({
|
|
1497
1519
|
//key: "\x1b[200~" + text + "\x1b[201~",
|
|
1498
1520
|
key: text,
|