pinokiod 3.183.0 → 3.185.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/kernel/api/terminal/index.js +65 -2
- package/package.json +1 -1
- package/server/index.js +74 -0
- package/server/public/common.js +198 -10
- package/server/views/layout.ejs +1 -1
- package/server/views/shell.ejs +256 -6
- package/server/views/terminal.ejs +269 -6
|
@@ -28,8 +28,20 @@ class Terminal {
|
|
|
28
28
|
await fs.promises.mkdir(uploadRoot, { recursive: true })
|
|
29
29
|
|
|
30
30
|
const saved = []
|
|
31
|
+
const failures = []
|
|
32
|
+
|
|
33
|
+
const remoteFiles = []
|
|
34
|
+
const localFiles = []
|
|
31
35
|
|
|
32
36
|
for (const file of files) {
|
|
37
|
+
if (file && typeof file.url === 'string' && file.url.trim().length > 0) {
|
|
38
|
+
remoteFiles.push(file)
|
|
39
|
+
} else {
|
|
40
|
+
localFiles.push(file)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const file of localFiles) {
|
|
33
45
|
const key = file && file.key
|
|
34
46
|
if (!key || !buffers[key]) {
|
|
35
47
|
continue
|
|
@@ -66,11 +78,62 @@ class Terminal {
|
|
|
66
78
|
|
|
67
79
|
}
|
|
68
80
|
|
|
81
|
+
for (const file of remoteFiles) {
|
|
82
|
+
const url = typeof file.url === 'string' ? file.url.trim() : ''
|
|
83
|
+
if (!url) {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
let originalName = typeof file.name === 'string' && file.name.trim().length > 0
|
|
87
|
+
? file.name.trim()
|
|
88
|
+
: null
|
|
89
|
+
if (!originalName) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = new URL(url)
|
|
92
|
+
const baseSegment = parsed.pathname ? parsed.pathname.split('/').filter(Boolean).pop() : null
|
|
93
|
+
originalName = baseSegment || 'download'
|
|
94
|
+
} catch (_) {
|
|
95
|
+
originalName = 'download'
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let sanitized = sanitize(originalName) || 'download'
|
|
99
|
+
const targetName = await this.uniqueFilename(uploadRoot, sanitized)
|
|
100
|
+
const targetPath = path.join(uploadRoot, targetName)
|
|
101
|
+
try {
|
|
102
|
+
await kernel.download({ uri: url, path: uploadRoot, filename: targetName }, ondata || (() => {}))
|
|
103
|
+
const stats = await fs.promises.stat(targetPath)
|
|
104
|
+
const size = stats.size
|
|
105
|
+
const homeRelativePath = path.relative(kernel.homedir, targetPath)
|
|
106
|
+
const normalizedHomeRelativePath = homeRelativePath.split(path.sep).join('/')
|
|
107
|
+
const cliPath = targetPath
|
|
108
|
+
const cliBase = baseCwd || kernel.homedir
|
|
109
|
+
const cliRelative = cliBase ? path.relative(cliBase, targetPath) : null
|
|
110
|
+
const cliRelativePath = cliRelative ? cliRelative.split(path.sep).join('/') : null
|
|
111
|
+
|
|
112
|
+
saved.push({
|
|
113
|
+
originalName,
|
|
114
|
+
storedAs: targetName,
|
|
115
|
+
path: targetPath,
|
|
116
|
+
size,
|
|
117
|
+
mimeType: typeof file.type === 'string' ? file.type : '',
|
|
118
|
+
homeRelativePath: normalizedHomeRelativePath,
|
|
119
|
+
displayPath: `~/${normalizedHomeRelativePath}`,
|
|
120
|
+
cliPath,
|
|
121
|
+
cliRelativePath,
|
|
122
|
+
sourceUrl: url
|
|
123
|
+
})
|
|
124
|
+
} catch (error) {
|
|
125
|
+
failures.push({ url, error: error.message })
|
|
126
|
+
try {
|
|
127
|
+
await fs.promises.rm(targetPath, { force: true })
|
|
128
|
+
} catch (_) {}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
69
132
|
if (saved.length === 0) {
|
|
70
133
|
if (ondata) {
|
|
71
134
|
ondata({ raw: "\r\nNo files were saved.\r\n" })
|
|
72
135
|
}
|
|
73
|
-
return { files: saved }
|
|
136
|
+
return { files: saved, errors: failures }
|
|
74
137
|
}
|
|
75
138
|
|
|
76
139
|
const marker = '[attachment] '
|
|
@@ -95,7 +158,7 @@ class Terminal {
|
|
|
95
158
|
req.params.buffers = {}
|
|
96
159
|
}
|
|
97
160
|
|
|
98
|
-
return { files: saved }
|
|
161
|
+
return { files: saved, errors: failures }
|
|
99
162
|
}
|
|
100
163
|
|
|
101
164
|
resolveShellInstance(params, kernel) {
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -26,6 +26,7 @@ const axios = require('axios')
|
|
|
26
26
|
const crypto = require('crypto')
|
|
27
27
|
const serveIndex = require('./serveIndex')
|
|
28
28
|
const registerFileRoutes = require('./routes/files')
|
|
29
|
+
const TerminalApi = require('../kernel/api/terminal')
|
|
29
30
|
|
|
30
31
|
const git = require('isomorphic-git')
|
|
31
32
|
const http = require('isomorphic-git/http/node')
|
|
@@ -4799,6 +4800,79 @@ class Server {
|
|
|
4799
4800
|
res.json({ error: e.stack })
|
|
4800
4801
|
}
|
|
4801
4802
|
}))
|
|
4803
|
+
this.app.post("/terminal/url-upload", ex(async (req, res) => {
|
|
4804
|
+
const payload = req.body || {}
|
|
4805
|
+
const id = typeof payload.id === 'string' ? payload.id.trim() : ''
|
|
4806
|
+
const cwd = typeof payload.cwd === 'string' ? payload.cwd : null
|
|
4807
|
+
const inputUrls = Array.isArray(payload.urls) ? payload.urls : []
|
|
4808
|
+
if (!id) {
|
|
4809
|
+
res.status(400).json({ error: 'terminal id is required' })
|
|
4810
|
+
return
|
|
4811
|
+
}
|
|
4812
|
+
if (inputUrls.length === 0) {
|
|
4813
|
+
res.status(400).json({ error: 'at least one url is required' })
|
|
4814
|
+
return
|
|
4815
|
+
}
|
|
4816
|
+
const normalized = []
|
|
4817
|
+
const hostHeader = typeof req.get === 'function' ? req.get('host') : null
|
|
4818
|
+
const baseOrigin = hostHeader ? `${req.protocol || 'http'}://${hostHeader}` : null
|
|
4819
|
+
const seen = new Set()
|
|
4820
|
+
for (const entry of inputUrls) {
|
|
4821
|
+
const rawHref = (entry && typeof entry === 'object') ? entry.href : entry
|
|
4822
|
+
const nameHint = entry && typeof entry === 'object' && typeof entry.name === 'string' ? entry.name : undefined
|
|
4823
|
+
if (typeof rawHref !== 'string') {
|
|
4824
|
+
continue
|
|
4825
|
+
}
|
|
4826
|
+
const trimmed = rawHref.trim()
|
|
4827
|
+
if (!trimmed) {
|
|
4828
|
+
continue
|
|
4829
|
+
}
|
|
4830
|
+
let resolved
|
|
4831
|
+
try {
|
|
4832
|
+
resolved = baseOrigin ? new URL(trimmed, baseOrigin) : new URL(trimmed)
|
|
4833
|
+
} catch (_) {
|
|
4834
|
+
try {
|
|
4835
|
+
resolved = new URL(trimmed)
|
|
4836
|
+
} catch (_) {
|
|
4837
|
+
continue
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
if (!resolved || !/^https?:$/i.test(resolved.protocol)) {
|
|
4841
|
+
continue
|
|
4842
|
+
}
|
|
4843
|
+
const href = resolved.href
|
|
4844
|
+
if (seen.has(href)) {
|
|
4845
|
+
continue
|
|
4846
|
+
}
|
|
4847
|
+
seen.add(href)
|
|
4848
|
+
const item = { url: href }
|
|
4849
|
+
if (nameHint && nameHint.trim()) {
|
|
4850
|
+
item.name = nameHint.trim()
|
|
4851
|
+
}
|
|
4852
|
+
normalized.push(item)
|
|
4853
|
+
}
|
|
4854
|
+
if (normalized.length === 0) {
|
|
4855
|
+
res.status(400).json({ error: 'no valid urls' })
|
|
4856
|
+
return
|
|
4857
|
+
}
|
|
4858
|
+
const terminalApi = new TerminalApi()
|
|
4859
|
+
const requestPayload = {
|
|
4860
|
+
params: {
|
|
4861
|
+
id,
|
|
4862
|
+
cwd,
|
|
4863
|
+
files: normalized,
|
|
4864
|
+
buffers: {}
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
try {
|
|
4868
|
+
const result = await terminalApi.upload(requestPayload, () => {}, this.kernel)
|
|
4869
|
+
const files = result && Array.isArray(result.files) ? result.files : []
|
|
4870
|
+
const errors = result && Array.isArray(result.errors) ? result.errors : []
|
|
4871
|
+
res.json({ files, errors })
|
|
4872
|
+
} catch (error) {
|
|
4873
|
+
res.status(500).json({ error: error && error.message ? error.message : 'remote upload failed' })
|
|
4874
|
+
}
|
|
4875
|
+
}))
|
|
4802
4876
|
this.app.post("/push", ex(async (req, res) => {
|
|
4803
4877
|
try {
|
|
4804
4878
|
const payload = { ...(req.body || {}) }
|
package/server/public/common.js
CHANGED
|
@@ -2190,21 +2190,209 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2190
2190
|
});
|
|
2191
2191
|
observer.observe(document.body, { attributes: true });
|
|
2192
2192
|
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2193
|
+
const createFrameHistoryController = () => {
|
|
2194
|
+
const sanitizeStack = (input) => {
|
|
2195
|
+
if (!Array.isArray(input)) {
|
|
2196
|
+
return []
|
|
2197
|
+
}
|
|
2198
|
+
return input.filter((value) => typeof value === 'string' && value.length > 0)
|
|
2199
|
+
}
|
|
2200
|
+
const resolveFrameKey = () => {
|
|
2201
|
+
try {
|
|
2202
|
+
if (typeof window.name === 'string' && window.name.length > 0) {
|
|
2203
|
+
return `name:${window.name}`
|
|
2204
|
+
}
|
|
2205
|
+
} catch (_) {}
|
|
2206
|
+
try {
|
|
2207
|
+
if (window.frameElement && window.frameElement.id) {
|
|
2208
|
+
return `frame:${window.frameElement.id}`
|
|
2209
|
+
}
|
|
2210
|
+
} catch (_) {}
|
|
2211
|
+
return 'top'
|
|
2212
|
+
}
|
|
2213
|
+
const frameKey = resolveFrameKey()
|
|
2214
|
+
const storageKey = `pinokio:frame-history:v1:${frameKey}`
|
|
2215
|
+
const MAX_ENTRIES = 64
|
|
2216
|
+
let storageFailed = false
|
|
2217
|
+
|
|
2218
|
+
const normalizeState = (value) => {
|
|
2219
|
+
const past = sanitizeStack(value && value.past)
|
|
2220
|
+
const future = sanitizeStack(value && value.future)
|
|
2221
|
+
const trimmedPast = past.length > MAX_ENTRIES ? past.slice(-MAX_ENTRIES) : past
|
|
2222
|
+
const trimmedFuture = future.length > MAX_ENTRIES ? future.slice(-MAX_ENTRIES) : future
|
|
2223
|
+
return { past: trimmedPast.slice(), future: trimmedFuture.slice() }
|
|
2224
|
+
}
|
|
2225
|
+
const readState = () => {
|
|
2226
|
+
if (storageFailed) {
|
|
2227
|
+
return { past: [], future: [] }
|
|
2228
|
+
}
|
|
2229
|
+
try {
|
|
2230
|
+
const raw = sessionStorage.getItem(storageKey)
|
|
2231
|
+
if (!raw) {
|
|
2232
|
+
return { past: [], future: [] }
|
|
2233
|
+
}
|
|
2234
|
+
return normalizeState(JSON.parse(raw))
|
|
2235
|
+
} catch (_) {
|
|
2236
|
+
storageFailed = true
|
|
2237
|
+
return { past: [], future: [] }
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const writeState = (state) => {
|
|
2241
|
+
if (storageFailed) {
|
|
2242
|
+
return false
|
|
2243
|
+
}
|
|
2244
|
+
try {
|
|
2245
|
+
sessionStorage.setItem(storageKey, JSON.stringify(normalizeState(state)))
|
|
2246
|
+
return true
|
|
2247
|
+
} catch (_) {
|
|
2248
|
+
storageFailed = true
|
|
2249
|
+
return false
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
const ensureCurrentRecorded = () => {
|
|
2254
|
+
const state = readState()
|
|
2255
|
+
if (storageFailed) {
|
|
2256
|
+
return
|
|
2257
|
+
}
|
|
2258
|
+
const currentUrl = window.location.href
|
|
2259
|
+
const last = state.past[state.past.length - 1]
|
|
2260
|
+
if (last !== currentUrl) {
|
|
2261
|
+
state.past.push(currentUrl)
|
|
2262
|
+
if (state.past.length > MAX_ENTRIES) {
|
|
2263
|
+
state.past = state.past.slice(-MAX_ENTRIES)
|
|
2264
|
+
}
|
|
2265
|
+
state.future = []
|
|
2266
|
+
if (!writeState(state)) {
|
|
2267
|
+
return
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
try {
|
|
2273
|
+
ensureCurrentRecorded()
|
|
2274
|
+
} catch (_) {
|
|
2275
|
+
storageFailed = true
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
if (storageFailed) {
|
|
2279
|
+
return null
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const navigateByDelta = (delta) => {
|
|
2283
|
+
if (!Number.isFinite(delta) || delta === 0) {
|
|
2284
|
+
return false
|
|
2285
|
+
}
|
|
2286
|
+
if (storageFailed) {
|
|
2287
|
+
return false
|
|
2288
|
+
}
|
|
2289
|
+
const state = readState()
|
|
2290
|
+
if (storageFailed) {
|
|
2291
|
+
return false
|
|
2292
|
+
}
|
|
2293
|
+
if (delta < 0) {
|
|
2294
|
+
const available = state.past.length - 1
|
|
2295
|
+
if (available <= 0) {
|
|
2296
|
+
return true
|
|
2297
|
+
}
|
|
2298
|
+
let steps = Math.min(-delta, available)
|
|
2299
|
+
while (steps > 0) {
|
|
2300
|
+
const current = state.past.pop()
|
|
2301
|
+
if (typeof current === 'string' && current.length > 0) {
|
|
2302
|
+
state.future.push(current)
|
|
2303
|
+
}
|
|
2304
|
+
steps -= 1
|
|
2305
|
+
}
|
|
2306
|
+
if (state.future.length > MAX_ENTRIES) {
|
|
2307
|
+
state.future = state.future.slice(-MAX_ENTRIES)
|
|
2308
|
+
}
|
|
2309
|
+
const target = state.past[state.past.length - 1]
|
|
2310
|
+
if (!target) {
|
|
2311
|
+
return false
|
|
2312
|
+
}
|
|
2313
|
+
if (!writeState(state)) {
|
|
2314
|
+
return false
|
|
2315
|
+
}
|
|
2316
|
+
try {
|
|
2317
|
+
window.location.replace(target)
|
|
2318
|
+
} catch (_) {
|
|
2319
|
+
window.location.href = target
|
|
2320
|
+
}
|
|
2321
|
+
return true
|
|
2322
|
+
}
|
|
2323
|
+
if (delta > 0) {
|
|
2324
|
+
if (state.future.length === 0) {
|
|
2325
|
+
return true
|
|
2326
|
+
}
|
|
2327
|
+
let steps = Math.min(delta, state.future.length)
|
|
2328
|
+
let target = null
|
|
2329
|
+
while (steps > 0) {
|
|
2330
|
+
target = state.future.pop() || target
|
|
2331
|
+
if (target) {
|
|
2332
|
+
state.past.push(target)
|
|
2333
|
+
}
|
|
2334
|
+
steps -= 1
|
|
2335
|
+
}
|
|
2336
|
+
if (!target) {
|
|
2337
|
+
return false
|
|
2338
|
+
}
|
|
2339
|
+
if (state.past.length > MAX_ENTRIES) {
|
|
2340
|
+
state.past = state.past.slice(-MAX_ENTRIES)
|
|
2341
|
+
}
|
|
2342
|
+
if (!writeState(state)) {
|
|
2343
|
+
return false
|
|
2344
|
+
}
|
|
2345
|
+
try {
|
|
2346
|
+
window.location.replace(target)
|
|
2347
|
+
} catch (_) {
|
|
2348
|
+
window.location.href = target
|
|
2349
|
+
}
|
|
2350
|
+
return true
|
|
2351
|
+
}
|
|
2352
|
+
return false
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
return {
|
|
2356
|
+
get enabled() {
|
|
2357
|
+
return !storageFailed
|
|
2358
|
+
},
|
|
2359
|
+
go: (delta) => {
|
|
2360
|
+
if (storageFailed) {
|
|
2361
|
+
return false
|
|
2362
|
+
}
|
|
2363
|
+
return navigateByDelta(delta)
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2197
2366
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2367
|
+
|
|
2368
|
+
const frameHistoryController = createFrameHistoryController()
|
|
2369
|
+
const bindHistoryButton = (selector, delta) => {
|
|
2370
|
+
const button = document.querySelector(selector)
|
|
2371
|
+
if (!button) {
|
|
2372
|
+
return
|
|
2373
|
+
}
|
|
2374
|
+
button.addEventListener("click", (event) => {
|
|
2375
|
+
if (frameHistoryController && frameHistoryController.enabled) {
|
|
2376
|
+
event.preventDefault()
|
|
2377
|
+
event.stopPropagation()
|
|
2378
|
+
frameHistoryController.go(delta)
|
|
2379
|
+
return
|
|
2380
|
+
}
|
|
2381
|
+
if (delta < 0) {
|
|
2382
|
+
history.back()
|
|
2383
|
+
} else if (delta > 0) {
|
|
2384
|
+
history.forward()
|
|
2385
|
+
}
|
|
2201
2386
|
})
|
|
2202
2387
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2388
|
+
|
|
2389
|
+
if (document.querySelector("#screenshot")) {
|
|
2390
|
+
document.querySelector("#screenshot").addEventListener("click", (e) => {
|
|
2391
|
+
screenshot()
|
|
2206
2392
|
})
|
|
2207
2393
|
}
|
|
2394
|
+
bindHistoryButton("#back", -1)
|
|
2395
|
+
bindHistoryButton("#forward", 1)
|
|
2208
2396
|
if (document.querySelector("#refresh-page")) {
|
|
2209
2397
|
document.querySelector("#refresh-page").addEventListener("click", (e) => {
|
|
2210
2398
|
try {
|
package/server/views/layout.ejs
CHANGED
package/server/views/shell.ejs
CHANGED
|
@@ -829,7 +829,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
829
829
|
await this.start(mode)
|
|
830
830
|
}
|
|
831
831
|
async uploadFiles(files, overlay) {
|
|
832
|
-
|
|
832
|
+
const localFiles = Array.isArray(files) ? files : []
|
|
833
|
+
if (localFiles.length === 0) {
|
|
833
834
|
return
|
|
834
835
|
}
|
|
835
836
|
if (!this.socket || !this.socket.ws || this.socket.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -840,8 +841,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
840
841
|
return
|
|
841
842
|
}
|
|
842
843
|
const entries = []
|
|
843
|
-
for (let i = 0; i <
|
|
844
|
-
const file =
|
|
844
|
+
for (let i = 0; i < localFiles.length; i++) {
|
|
845
|
+
const file = localFiles[i]
|
|
845
846
|
if (!file || typeof file.arrayBuffer !== "function") {
|
|
846
847
|
continue
|
|
847
848
|
}
|
|
@@ -893,6 +894,226 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
893
894
|
}
|
|
894
895
|
}
|
|
895
896
|
}
|
|
897
|
+
async uploadRemoteResources(resources, overlay) {
|
|
898
|
+
const remoteItems = Array.isArray(resources) ? resources.filter((item) => item && typeof item.href === "string" && item.href.trim()) : []
|
|
899
|
+
if (remoteItems.length === 0) {
|
|
900
|
+
return
|
|
901
|
+
}
|
|
902
|
+
const payload = {
|
|
903
|
+
id: shell_id,
|
|
904
|
+
cwd: this.uploadContext.cwd,
|
|
905
|
+
urls: remoteItems
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
if (overlay) {
|
|
909
|
+
overlay.classList.add("active")
|
|
910
|
+
overlay.textContent = "Downloading..."
|
|
911
|
+
}
|
|
912
|
+
const response = await fetch('/terminal/url-upload', {
|
|
913
|
+
method: 'POST',
|
|
914
|
+
headers: {
|
|
915
|
+
'Content-Type': 'application/json'
|
|
916
|
+
},
|
|
917
|
+
body: JSON.stringify(payload)
|
|
918
|
+
})
|
|
919
|
+
if (!response.ok) {
|
|
920
|
+
throw new Error(`HTTP ${response.status}`)
|
|
921
|
+
}
|
|
922
|
+
const result = await response.json().catch(() => ({}))
|
|
923
|
+
const files = Array.isArray(result.files) ? result.files : []
|
|
924
|
+
const failures = Array.isArray(result.errors) ? result.errors : []
|
|
925
|
+
if (files.length > 0) {
|
|
926
|
+
try {
|
|
927
|
+
refreshParent({
|
|
928
|
+
type: "terminal.upload",
|
|
929
|
+
files
|
|
930
|
+
})
|
|
931
|
+
if (typeof reloadMemory === 'function') {
|
|
932
|
+
reloadMemory()
|
|
933
|
+
}
|
|
934
|
+
} catch (_) {}
|
|
935
|
+
n.Noty({
|
|
936
|
+
text: files.length > 1 ? `${files.length} files attached` : `File attached`,
|
|
937
|
+
timeout: 4000
|
|
938
|
+
})
|
|
939
|
+
}
|
|
940
|
+
if (failures.length > 0) {
|
|
941
|
+
const plural = failures.length > 1
|
|
942
|
+
n.Noty({
|
|
943
|
+
text: plural ? `${failures.length} remote files failed to download` : `Remote file failed to download`,
|
|
944
|
+
type: "error",
|
|
945
|
+
timeout: 5000
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
} catch (error) {
|
|
949
|
+
n.Noty({
|
|
950
|
+
text: `Remote upload failed: ${error.message}`,
|
|
951
|
+
type: "error"
|
|
952
|
+
})
|
|
953
|
+
} finally {
|
|
954
|
+
if (overlay) {
|
|
955
|
+
overlay.classList.remove("active")
|
|
956
|
+
overlay.textContent = "Drop files to upload"
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async collectFilesFromDataTransfer(dataTransfer) {
|
|
961
|
+
if (!dataTransfer) {
|
|
962
|
+
return { urls: [] }
|
|
963
|
+
}
|
|
964
|
+
const resourceMap = new Map()
|
|
965
|
+
const ensureResource = (input, nameHint) => {
|
|
966
|
+
if (!input) {
|
|
967
|
+
return
|
|
968
|
+
}
|
|
969
|
+
let resolved
|
|
970
|
+
try {
|
|
971
|
+
resolved = new URL(input, window.location.href)
|
|
972
|
+
} catch (_) {
|
|
973
|
+
return
|
|
974
|
+
}
|
|
975
|
+
if (!resolved || !resolved.protocol || !/^https?:$/i.test(resolved.protocol)) {
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
const href = resolved.href
|
|
979
|
+
const existing = resourceMap.get(href)
|
|
980
|
+
if (existing) {
|
|
981
|
+
if (nameHint && !existing.nameHint) {
|
|
982
|
+
existing.nameHint = nameHint
|
|
983
|
+
}
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
resourceMap.set(href, { href, url: resolved, nameHint: nameHint || null })
|
|
987
|
+
}
|
|
988
|
+
const handleDownloadUrl = (value) => {
|
|
989
|
+
if (!value) {
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
const firstColon = value.indexOf(":")
|
|
993
|
+
const lastColon = value.lastIndexOf(":")
|
|
994
|
+
if (firstColon === -1 || lastColon === -1 || lastColon <= firstColon) {
|
|
995
|
+
ensureResource(value, null)
|
|
996
|
+
return
|
|
997
|
+
}
|
|
998
|
+
const filename = value.slice(firstColon + 1, lastColon)
|
|
999
|
+
const url = value.slice(lastColon + 1)
|
|
1000
|
+
ensureResource(url, filename)
|
|
1001
|
+
}
|
|
1002
|
+
const handleStringEntry = (type, rawValue) => {
|
|
1003
|
+
if (!rawValue) {
|
|
1004
|
+
return
|
|
1005
|
+
}
|
|
1006
|
+
if (type === "DownloadURL") {
|
|
1007
|
+
handleDownloadUrl(rawValue)
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
1010
|
+
const value = rawValue.trim()
|
|
1011
|
+
if (!value) {
|
|
1012
|
+
return
|
|
1013
|
+
}
|
|
1014
|
+
if (type === "text/html") {
|
|
1015
|
+
try {
|
|
1016
|
+
const parser = new DOMParser()
|
|
1017
|
+
const doc = parser.parseFromString(value, "text/html")
|
|
1018
|
+
const anchors = doc.querySelectorAll("a[href]")
|
|
1019
|
+
anchors.forEach((anchor) => {
|
|
1020
|
+
const href = anchor.getAttribute("href")
|
|
1021
|
+
const nameHint = anchor.getAttribute("download") || (anchor.textContent ? anchor.textContent.trim() : null)
|
|
1022
|
+
ensureResource(href, nameHint)
|
|
1023
|
+
})
|
|
1024
|
+
const srcElements = doc.querySelectorAll("[src]")
|
|
1025
|
+
srcElements.forEach((element) => {
|
|
1026
|
+
const src = element.getAttribute("src")
|
|
1027
|
+
if (!src) {
|
|
1028
|
+
return
|
|
1029
|
+
}
|
|
1030
|
+
const title = element.getAttribute("download") || element.getAttribute("title") || element.getAttribute("alt") || null
|
|
1031
|
+
ensureResource(src, title)
|
|
1032
|
+
})
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
console.warn("Failed to parse dropped HTML for resources", error)
|
|
1035
|
+
}
|
|
1036
|
+
return
|
|
1037
|
+
}
|
|
1038
|
+
if (type === "text/x-moz-url") {
|
|
1039
|
+
const parts = value.split(/\r?\n/)
|
|
1040
|
+
const url = parts[0] ? parts[0].trim() : ""
|
|
1041
|
+
const title = parts[1] ? parts[1].trim() : null
|
|
1042
|
+
if (url) {
|
|
1043
|
+
ensureResource(url, title)
|
|
1044
|
+
}
|
|
1045
|
+
return
|
|
1046
|
+
}
|
|
1047
|
+
const lines = value.split(/\r?\n/)
|
|
1048
|
+
for (const line of lines) {
|
|
1049
|
+
const candidate = line.trim()
|
|
1050
|
+
if (!candidate || candidate.startsWith("#")) {
|
|
1051
|
+
continue
|
|
1052
|
+
}
|
|
1053
|
+
let resolvedHref
|
|
1054
|
+
try {
|
|
1055
|
+
const resolved = new URL(candidate, window.location.href)
|
|
1056
|
+
resolvedHref = resolved.href
|
|
1057
|
+
} catch (_) {
|
|
1058
|
+
continue
|
|
1059
|
+
}
|
|
1060
|
+
let decodedHref = resolvedHref
|
|
1061
|
+
try {
|
|
1062
|
+
decodedHref = decodeURIComponent(resolvedHref)
|
|
1063
|
+
} catch (_) {}
|
|
1064
|
+
if (/[<>"']/.test(decodedHref)) {
|
|
1065
|
+
continue
|
|
1066
|
+
}
|
|
1067
|
+
ensureResource(resolvedHref, null)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const items = dataTransfer.items ? Array.from(dataTransfer.items) : []
|
|
1071
|
+
const stringItems = items.filter((item) => item && item.kind === "string" && typeof item.getAsString === "function")
|
|
1072
|
+
if (stringItems.length > 0) {
|
|
1073
|
+
const stringValues = await Promise.all(stringItems.map((item) => new Promise((resolve) => {
|
|
1074
|
+
try {
|
|
1075
|
+
item.getAsString((value) => resolve({ type: item.type || "text/plain", value }))
|
|
1076
|
+
} catch (_) {
|
|
1077
|
+
resolve(null)
|
|
1078
|
+
}
|
|
1079
|
+
})))
|
|
1080
|
+
for (const entry of stringValues) {
|
|
1081
|
+
if (entry) {
|
|
1082
|
+
handleStringEntry(entry.type, entry.value)
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const fallbackTypes = ["DownloadURL", "text/uri-list", "text/plain", "text/html", "text/x-moz-url"]
|
|
1087
|
+
if (typeof dataTransfer.getData === "function") {
|
|
1088
|
+
for (const type of fallbackTypes) {
|
|
1089
|
+
try {
|
|
1090
|
+
const value = dataTransfer.getData(type)
|
|
1091
|
+
if (value) {
|
|
1092
|
+
handleStringEntry(type, value)
|
|
1093
|
+
}
|
|
1094
|
+
} catch (_) {}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const resources = Array.from(resourceMap.values())
|
|
1098
|
+
if (!resources.length) {
|
|
1099
|
+
return { urls: [] }
|
|
1100
|
+
}
|
|
1101
|
+
const urls = resources.map((resource) => {
|
|
1102
|
+
const basePath = resource.url ? resource.url.pathname : new URL(resource.href).pathname
|
|
1103
|
+
let filename = resource.nameHint || basePath.split("/").pop() || "download"
|
|
1104
|
+
try {
|
|
1105
|
+
filename = decodeURIComponent(filename)
|
|
1106
|
+
} catch (_) {}
|
|
1107
|
+
if (!filename) {
|
|
1108
|
+
filename = "download"
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
href: resource.href,
|
|
1112
|
+
name: filename
|
|
1113
|
+
}
|
|
1114
|
+
})
|
|
1115
|
+
return { urls }
|
|
1116
|
+
}
|
|
896
1117
|
async createTerm (_theme) {
|
|
897
1118
|
console.log(xtermTheme)
|
|
898
1119
|
if (!this.term) {
|
|
@@ -964,11 +1185,40 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
964
1185
|
prevent(event)
|
|
965
1186
|
dragDepth = 0
|
|
966
1187
|
dropOverlay.classList.remove("active")
|
|
967
|
-
const
|
|
968
|
-
|
|
1188
|
+
const files = Array.from(event.dataTransfer ? event.dataTransfer.files || [] : [])
|
|
1189
|
+
let remoteResources = []
|
|
1190
|
+
try {
|
|
1191
|
+
const extra = await this.collectFilesFromDataTransfer(event.dataTransfer)
|
|
1192
|
+
if (extra && Array.isArray(extra.urls) && extra.urls.length) {
|
|
1193
|
+
const seenUrls = new Set()
|
|
1194
|
+
remoteResources = extra.urls.filter((item) => {
|
|
1195
|
+
if (!item || typeof item.href !== "string") {
|
|
1196
|
+
return false
|
|
1197
|
+
}
|
|
1198
|
+
const key = item.href.trim()
|
|
1199
|
+
if (!key || seenUrls.has(key)) {
|
|
1200
|
+
return false
|
|
1201
|
+
}
|
|
1202
|
+
seenUrls.add(key)
|
|
1203
|
+
return true
|
|
1204
|
+
})
|
|
1205
|
+
}
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
console.warn("Failed to collect files from drop payload", error)
|
|
1208
|
+
}
|
|
1209
|
+
if (!files.length && (!remoteResources || remoteResources.length === 0)) {
|
|
969
1210
|
return
|
|
970
1211
|
}
|
|
971
|
-
|
|
1212
|
+
try {
|
|
1213
|
+
if (remoteResources && remoteResources.length > 0) {
|
|
1214
|
+
await this.uploadRemoteResources(remoteResources, dropOverlay)
|
|
1215
|
+
}
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
console.warn("Remote upload failed", error)
|
|
1218
|
+
}
|
|
1219
|
+
if (files.length > 0) {
|
|
1220
|
+
await this.uploadFiles(files, dropOverlay)
|
|
1221
|
+
}
|
|
972
1222
|
this.term.focus()
|
|
973
1223
|
})
|
|
974
1224
|
term.attachCustomKeyEventHandler(event => {
|
|
@@ -916,7 +916,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
916
916
|
await this.start(mode)
|
|
917
917
|
}
|
|
918
918
|
async uploadFiles(files, overlay) {
|
|
919
|
-
|
|
919
|
+
const localFiles = Array.isArray(files) ? files : []
|
|
920
|
+
if (localFiles.length === 0) {
|
|
920
921
|
return
|
|
921
922
|
}
|
|
922
923
|
if (!this.socket || !this.socket.ws || this.socket.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -927,8 +928,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
927
928
|
return
|
|
928
929
|
}
|
|
929
930
|
const entries = []
|
|
930
|
-
for (let i = 0; i <
|
|
931
|
-
const file =
|
|
931
|
+
for (let i = 0; i < localFiles.length; i++) {
|
|
932
|
+
const file = localFiles[i]
|
|
932
933
|
if (!file || typeof file.arrayBuffer !== "function") {
|
|
933
934
|
continue
|
|
934
935
|
}
|
|
@@ -980,6 +981,235 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
980
981
|
}
|
|
981
982
|
}
|
|
982
983
|
}
|
|
984
|
+
async uploadRemoteResources(resources, overlay) {
|
|
985
|
+
const remoteItems = Array.isArray(resources) ? resources.filter((item) => item && typeof item.href === "string" && item.href.trim()) : []
|
|
986
|
+
if (remoteItems.length === 0) {
|
|
987
|
+
return
|
|
988
|
+
}
|
|
989
|
+
const payload = {
|
|
990
|
+
id: shell_id,
|
|
991
|
+
cwd: this.uploadContext.cwd,
|
|
992
|
+
urls: remoteItems
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
if (overlay) {
|
|
996
|
+
overlay.classList.add("active")
|
|
997
|
+
overlay.textContent = "Downloading..."
|
|
998
|
+
}
|
|
999
|
+
const response = await fetch('/terminal/url-upload', {
|
|
1000
|
+
method: 'POST',
|
|
1001
|
+
headers: {
|
|
1002
|
+
'Content-Type': 'application/json'
|
|
1003
|
+
},
|
|
1004
|
+
body: JSON.stringify(payload)
|
|
1005
|
+
})
|
|
1006
|
+
if (!response.ok) {
|
|
1007
|
+
throw new Error(`HTTP ${response.status}`)
|
|
1008
|
+
}
|
|
1009
|
+
const result = await response.json().catch(() => ({}))
|
|
1010
|
+
const files = Array.isArray(result.files) ? result.files : []
|
|
1011
|
+
const failures = Array.isArray(result.errors) ? result.errors : []
|
|
1012
|
+
if (files.length > 0) {
|
|
1013
|
+
const mappedFiles = files.map((file) => ({
|
|
1014
|
+
originalName: file.originalName || file.name || file.storedAs,
|
|
1015
|
+
storedAs: file.storedAs,
|
|
1016
|
+
path: file.path,
|
|
1017
|
+
displayPath: file.displayPath || file.path,
|
|
1018
|
+
homeRelativePath: file.homeRelativePath || '',
|
|
1019
|
+
cliPath: file.cliPath || null,
|
|
1020
|
+
cliRelativePath: file.cliRelativePath || null
|
|
1021
|
+
}))
|
|
1022
|
+
try {
|
|
1023
|
+
refreshParent({
|
|
1024
|
+
type: "terminal.upload",
|
|
1025
|
+
files: mappedFiles
|
|
1026
|
+
})
|
|
1027
|
+
if (typeof reloadMemory === 'function') {
|
|
1028
|
+
reloadMemory()
|
|
1029
|
+
}
|
|
1030
|
+
} catch (_) {}
|
|
1031
|
+
n.Noty({
|
|
1032
|
+
text: mappedFiles.length > 1 ? `${mappedFiles.length} files attached` : `File attached`,
|
|
1033
|
+
timeout: 4000
|
|
1034
|
+
})
|
|
1035
|
+
}
|
|
1036
|
+
if (failures.length > 0) {
|
|
1037
|
+
const plural = failures.length > 1
|
|
1038
|
+
n.Noty({
|
|
1039
|
+
text: plural ? `${failures.length} remote files failed to download` : `Remote file failed to download`,
|
|
1040
|
+
type: "error",
|
|
1041
|
+
timeout: 5000
|
|
1042
|
+
})
|
|
1043
|
+
}
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
n.Noty({
|
|
1046
|
+
text: `Remote upload failed: ${error.message}`,
|
|
1047
|
+
type: "error"
|
|
1048
|
+
})
|
|
1049
|
+
} finally {
|
|
1050
|
+
if (overlay) {
|
|
1051
|
+
overlay.classList.remove("active")
|
|
1052
|
+
overlay.textContent = "Drop files to upload"
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
async collectFilesFromDataTransfer(dataTransfer) {
|
|
1057
|
+
if (!dataTransfer) {
|
|
1058
|
+
return { urls: [] }
|
|
1059
|
+
}
|
|
1060
|
+
const resourceMap = new Map()
|
|
1061
|
+
const ensureResource = (input, nameHint) => {
|
|
1062
|
+
if (!input) {
|
|
1063
|
+
return
|
|
1064
|
+
}
|
|
1065
|
+
let resolved
|
|
1066
|
+
try {
|
|
1067
|
+
resolved = new URL(input, window.location.href)
|
|
1068
|
+
} catch (_) {
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
if (!resolved || !resolved.protocol || !/^https?:$/i.test(resolved.protocol)) {
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
1074
|
+
const href = resolved.href
|
|
1075
|
+
const existing = resourceMap.get(href)
|
|
1076
|
+
if (existing) {
|
|
1077
|
+
if (nameHint && !existing.nameHint) {
|
|
1078
|
+
existing.nameHint = nameHint
|
|
1079
|
+
}
|
|
1080
|
+
return
|
|
1081
|
+
}
|
|
1082
|
+
resourceMap.set(href, { href, url: resolved, nameHint: nameHint || null })
|
|
1083
|
+
}
|
|
1084
|
+
const handleDownloadUrl = (value) => {
|
|
1085
|
+
if (!value) {
|
|
1086
|
+
return
|
|
1087
|
+
}
|
|
1088
|
+
const firstColon = value.indexOf(":")
|
|
1089
|
+
const lastColon = value.lastIndexOf(":")
|
|
1090
|
+
if (firstColon === -1 || lastColon === -1 || lastColon <= firstColon) {
|
|
1091
|
+
ensureResource(value, null)
|
|
1092
|
+
return
|
|
1093
|
+
}
|
|
1094
|
+
const filename = value.slice(firstColon + 1, lastColon)
|
|
1095
|
+
const url = value.slice(lastColon + 1)
|
|
1096
|
+
ensureResource(url, filename)
|
|
1097
|
+
}
|
|
1098
|
+
const handleStringEntry = (type, rawValue) => {
|
|
1099
|
+
if (!rawValue) {
|
|
1100
|
+
return
|
|
1101
|
+
}
|
|
1102
|
+
if (type === "DownloadURL") {
|
|
1103
|
+
handleDownloadUrl(rawValue)
|
|
1104
|
+
return
|
|
1105
|
+
}
|
|
1106
|
+
const value = rawValue.trim()
|
|
1107
|
+
if (!value) {
|
|
1108
|
+
return
|
|
1109
|
+
}
|
|
1110
|
+
if (type === "text/html") {
|
|
1111
|
+
try {
|
|
1112
|
+
const parser = new DOMParser()
|
|
1113
|
+
const doc = parser.parseFromString(value, "text/html")
|
|
1114
|
+
const anchors = doc.querySelectorAll("a[href]")
|
|
1115
|
+
anchors.forEach((anchor) => {
|
|
1116
|
+
const href = anchor.getAttribute("href")
|
|
1117
|
+
const nameHint = anchor.getAttribute("download") || (anchor.textContent ? anchor.textContent.trim() : null)
|
|
1118
|
+
ensureResource(href, nameHint)
|
|
1119
|
+
})
|
|
1120
|
+
const srcElements = doc.querySelectorAll("[src]")
|
|
1121
|
+
srcElements.forEach((element) => {
|
|
1122
|
+
const src = element.getAttribute("src")
|
|
1123
|
+
if (!src) {
|
|
1124
|
+
return
|
|
1125
|
+
}
|
|
1126
|
+
const title = element.getAttribute("download") || element.getAttribute("title") || element.getAttribute("alt") || null
|
|
1127
|
+
ensureResource(src, title)
|
|
1128
|
+
})
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
console.warn("Failed to parse dropped HTML for resources", error)
|
|
1131
|
+
}
|
|
1132
|
+
return
|
|
1133
|
+
}
|
|
1134
|
+
if (type === "text/x-moz-url") {
|
|
1135
|
+
const parts = value.split(/\r?\n/)
|
|
1136
|
+
const url = parts[0] ? parts[0].trim() : ""
|
|
1137
|
+
const title = parts[1] ? parts[1].trim() : null
|
|
1138
|
+
if (url) {
|
|
1139
|
+
ensureResource(url, title)
|
|
1140
|
+
}
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
const lines = value.split(/\r?\n/)
|
|
1144
|
+
for (const line of lines) {
|
|
1145
|
+
const candidate = line.trim()
|
|
1146
|
+
if (!candidate || candidate.startsWith("#")) {
|
|
1147
|
+
continue
|
|
1148
|
+
}
|
|
1149
|
+
let resolvedHref
|
|
1150
|
+
try {
|
|
1151
|
+
const resolved = new URL(candidate, window.location.href)
|
|
1152
|
+
resolvedHref = resolved.href
|
|
1153
|
+
} catch (_) {
|
|
1154
|
+
continue
|
|
1155
|
+
}
|
|
1156
|
+
let decodedHref = resolvedHref
|
|
1157
|
+
try {
|
|
1158
|
+
decodedHref = decodeURIComponent(resolvedHref)
|
|
1159
|
+
} catch (_) {}
|
|
1160
|
+
if (/[<>"']/.test(decodedHref)) {
|
|
1161
|
+
continue
|
|
1162
|
+
}
|
|
1163
|
+
ensureResource(resolvedHref, null)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const items = dataTransfer.items ? Array.from(dataTransfer.items) : []
|
|
1167
|
+
const stringItems = items.filter((item) => item && item.kind === "string" && typeof item.getAsString === "function")
|
|
1168
|
+
if (stringItems.length > 0) {
|
|
1169
|
+
const stringValues = await Promise.all(stringItems.map((item) => new Promise((resolve) => {
|
|
1170
|
+
try {
|
|
1171
|
+
item.getAsString((value) => resolve({ type: item.type || "text/plain", value }))
|
|
1172
|
+
} catch (_) {
|
|
1173
|
+
resolve(null)
|
|
1174
|
+
}
|
|
1175
|
+
})))
|
|
1176
|
+
for (const entry of stringValues) {
|
|
1177
|
+
if (entry) {
|
|
1178
|
+
handleStringEntry(entry.type, entry.value)
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const fallbackTypes = ["DownloadURL", "text/uri-list", "text/plain", "text/html", "text/x-moz-url"]
|
|
1183
|
+
if (typeof dataTransfer.getData === "function") {
|
|
1184
|
+
for (const type of fallbackTypes) {
|
|
1185
|
+
try {
|
|
1186
|
+
const value = dataTransfer.getData(type)
|
|
1187
|
+
if (value) {
|
|
1188
|
+
handleStringEntry(type, value)
|
|
1189
|
+
}
|
|
1190
|
+
} catch (_) {}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const resources = Array.from(resourceMap.values())
|
|
1194
|
+
if (!resources.length) {
|
|
1195
|
+
return { urls: [] }
|
|
1196
|
+
}
|
|
1197
|
+
const urls = resources.map((resource) => {
|
|
1198
|
+
const basePath = resource.url ? resource.url.pathname : new URL(resource.href).pathname
|
|
1199
|
+
let filename = resource.nameHint || basePath.split("/").pop() || "download"
|
|
1200
|
+
try {
|
|
1201
|
+
filename = decodeURIComponent(filename)
|
|
1202
|
+
} catch (_) {}
|
|
1203
|
+
if (!filename) {
|
|
1204
|
+
filename = "download"
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
href: resource.href,
|
|
1208
|
+
name: filename
|
|
1209
|
+
}
|
|
1210
|
+
})
|
|
1211
|
+
return { urls }
|
|
1212
|
+
}
|
|
983
1213
|
async createTerm (_theme) {
|
|
984
1214
|
if (!this.term) {
|
|
985
1215
|
const theme = Object.assign({ }, _theme, {
|
|
@@ -1048,11 +1278,44 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1048
1278
|
prevent(event)
|
|
1049
1279
|
dragDepth = 0
|
|
1050
1280
|
dropOverlay.classList.remove("active")
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1281
|
+
const files = Array.from(event.dataTransfer ? event.dataTransfer.files || [] : [])
|
|
1282
|
+
let remoteResources = []
|
|
1283
|
+
try {
|
|
1284
|
+
const extra = await this.collectFilesFromDataTransfer(event.dataTransfer)
|
|
1285
|
+
if (extra && Array.isArray(extra.urls) && extra.urls.length) {
|
|
1286
|
+
const seenUrls = new Set()
|
|
1287
|
+
remoteResources = extra.urls.filter((item) => {
|
|
1288
|
+
if (!item || typeof item.href !== "string") {
|
|
1289
|
+
return false
|
|
1290
|
+
}
|
|
1291
|
+
const key = item.href.trim()
|
|
1292
|
+
if (!key || seenUrls.has(key)) {
|
|
1293
|
+
return false
|
|
1294
|
+
}
|
|
1295
|
+
seenUrls.add(key)
|
|
1296
|
+
return true
|
|
1297
|
+
})
|
|
1298
|
+
}
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
console.warn("Failed to collect files from drop payload", error)
|
|
1301
|
+
}
|
|
1302
|
+
if (!files.length && (!remoteResources || remoteResources.length === 0)) {
|
|
1303
|
+
n.Noty({
|
|
1304
|
+
text: "Dropped item did not include an accessible file",
|
|
1305
|
+
type: "error"
|
|
1306
|
+
})
|
|
1053
1307
|
return
|
|
1054
1308
|
}
|
|
1055
|
-
|
|
1309
|
+
try {
|
|
1310
|
+
if (remoteResources && remoteResources.length > 0) {
|
|
1311
|
+
await this.uploadRemoteResources(remoteResources, dropOverlay)
|
|
1312
|
+
}
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
console.warn("Remote upload failed", error)
|
|
1315
|
+
}
|
|
1316
|
+
if (files.length > 0) {
|
|
1317
|
+
await this.uploadFiles(files, dropOverlay)
|
|
1318
|
+
}
|
|
1056
1319
|
this.term.focus()
|
|
1057
1320
|
})
|
|
1058
1321
|
term.attachCustomKeyEventHandler(event => {
|