pinokiod 3.184.0 → 3.186.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/public/style.css +4 -0
- package/server/views/app.ejs +5 -1
- package/server/views/layout.ejs +1 -1
- package/server/views/shell.ejs +224 -76
- package/server/views/terminal.ejs +237 -76
|
@@ -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/public/style.css
CHANGED
|
@@ -295,8 +295,12 @@ body.dark .navheader2 .btn {
|
|
|
295
295
|
background: #F1F1F1 !important;
|
|
296
296
|
*/
|
|
297
297
|
}
|
|
298
|
+
body.dark .navheader {
|
|
299
|
+
background: rgba(255, 255, 255, 0.1);
|
|
300
|
+
}
|
|
298
301
|
.navheader {
|
|
299
302
|
backdrop-filter: blur(16px);
|
|
303
|
+
background: rgba(0, 0, 0, 0.04);
|
|
300
304
|
|
|
301
305
|
/*
|
|
302
306
|
padding: 15px 10px 5px;
|
package/server/views/app.ejs
CHANGED
|
@@ -165,7 +165,11 @@ body.dark .appcanvas_filler {
|
|
|
165
165
|
flex-direction: column;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
body.dark .appcanvas > aside {
|
|
169
|
+
background: rgba(255, 255, 255, 0.1);
|
|
170
|
+
}
|
|
168
171
|
.appcanvas > aside {
|
|
172
|
+
background: rgba(0, 0, 0, 0.04);
|
|
169
173
|
order: 0;
|
|
170
174
|
position: relative;
|
|
171
175
|
display: flex;
|
|
@@ -444,7 +448,7 @@ body.dark .appcanvas > aside .header-item.selected {
|
|
|
444
448
|
border-color: var(--sidebar-tab-outline);
|
|
445
449
|
*/
|
|
446
450
|
box-shadow: 0 4px 12px var(--pinokio-sidebar-tab-shadow);
|
|
447
|
-
background:
|
|
451
|
+
background: rgba(0,0,0,0.08) !important;
|
|
448
452
|
border-bottom: none;
|
|
449
453
|
z-index: 1;
|
|
450
454
|
}
|
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,92 +894,225 @@ 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
|
+
}
|
|
896
960
|
async collectFilesFromDataTransfer(dataTransfer) {
|
|
897
961
|
if (!dataTransfer) {
|
|
898
|
-
return []
|
|
962
|
+
return { urls: [] }
|
|
899
963
|
}
|
|
900
|
-
const
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const stringValues = await Promise.all(stringItems.map((item) => new Promise((resolve) => {
|
|
905
|
-
try {
|
|
906
|
-
item.getAsString((value) => resolve({ type: item.type || "text/plain", value }))
|
|
907
|
-
} catch (_) {
|
|
908
|
-
resolve({ type: item.type || "text/plain", value: "" })
|
|
909
|
-
}
|
|
910
|
-
})))
|
|
911
|
-
for (const entry of stringValues) {
|
|
912
|
-
if (entry && typeof entry.value === "string" && entry.value.trim()) {
|
|
913
|
-
collectedStrings.push(entry)
|
|
914
|
-
}
|
|
964
|
+
const resourceMap = new Map()
|
|
965
|
+
const ensureResource = (input, nameHint) => {
|
|
966
|
+
if (!input) {
|
|
967
|
+
return
|
|
915
968
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
969
|
+
let resolved
|
|
970
|
+
try {
|
|
971
|
+
resolved = new URL(input, window.location.href)
|
|
972
|
+
} catch (_) {
|
|
973
|
+
return
|
|
920
974
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
collectedStrings.push({ type: "text/plain", value: plain })
|
|
975
|
+
if (!resolved || !resolved.protocol || !/^https?:$/i.test(resolved.protocol)) {
|
|
976
|
+
return
|
|
924
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 })
|
|
925
987
|
}
|
|
926
|
-
|
|
927
|
-
|
|
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)
|
|
928
1001
|
}
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
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()
|
|
932
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("#")) {
|
|
933
1051
|
continue
|
|
934
1052
|
}
|
|
935
|
-
|
|
936
|
-
|
|
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)) {
|
|
937
1065
|
continue
|
|
938
1066
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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) => {
|
|
942
1074
|
try {
|
|
943
|
-
|
|
1075
|
+
item.getAsString((value) => resolve({ type: item.type || "text/plain", value }))
|
|
944
1076
|
} catch (_) {
|
|
945
|
-
|
|
1077
|
+
resolve(null)
|
|
946
1078
|
}
|
|
947
|
-
|
|
948
|
-
|
|
1079
|
+
})))
|
|
1080
|
+
for (const entry of stringValues) {
|
|
1081
|
+
if (entry) {
|
|
1082
|
+
handleStringEntry(entry.type, entry.value)
|
|
949
1083
|
}
|
|
950
|
-
urls.add(resolved.href)
|
|
951
1084
|
}
|
|
952
1085
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const FileCtor = typeof window !== "undefined" ? window.File : null
|
|
957
|
-
if (typeof FileCtor !== "function") {
|
|
958
|
-
return []
|
|
959
|
-
}
|
|
960
|
-
const files = []
|
|
961
|
-
for (const href of urls) {
|
|
962
|
-
try {
|
|
963
|
-
const response = await fetch(href, { credentials: "include" })
|
|
964
|
-
if (!response || !response.ok) {
|
|
965
|
-
continue
|
|
966
|
-
}
|
|
967
|
-
const blob = await response.blob()
|
|
968
|
-
const nameSegment = href.split("/").pop() || "download"
|
|
969
|
-
let filename = nameSegment || "download"
|
|
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) {
|
|
970
1089
|
try {
|
|
971
|
-
|
|
1090
|
+
const value = dataTransfer.getData(type)
|
|
1091
|
+
if (value) {
|
|
1092
|
+
handleStringEntry(type, value)
|
|
1093
|
+
}
|
|
972
1094
|
} catch (_) {}
|
|
973
|
-
if (!filename) {
|
|
974
|
-
filename = "download"
|
|
975
|
-
}
|
|
976
|
-
files.push(new FileCtor([blob], filename, { type: blob.type || "application/octet-stream", lastModified: Date.now() }))
|
|
977
|
-
} catch (error) {
|
|
978
|
-
console.warn("Failed to resolve dropped resource", href, error)
|
|
979
1095
|
}
|
|
980
1096
|
}
|
|
981
|
-
|
|
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 }
|
|
982
1116
|
}
|
|
983
1117
|
async createTerm (_theme) {
|
|
984
1118
|
console.log(xtermTheme)
|
|
@@ -1052,25 +1186,39 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1052
1186
|
dragDepth = 0
|
|
1053
1187
|
dropOverlay.classList.remove("active")
|
|
1054
1188
|
const files = Array.from(event.dataTransfer ? event.dataTransfer.files || [] : [])
|
|
1189
|
+
let remoteResources = []
|
|
1055
1190
|
try {
|
|
1056
1191
|
const extra = await this.collectFilesFromDataTransfer(event.dataTransfer)
|
|
1057
|
-
if (Array.isArray(extra) && extra.length) {
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
seen.add(key)
|
|
1063
|
-
files.push(file)
|
|
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
|
|
1064
1197
|
}
|
|
1065
|
-
|
|
1198
|
+
const key = item.href.trim()
|
|
1199
|
+
if (!key || seenUrls.has(key)) {
|
|
1200
|
+
return false
|
|
1201
|
+
}
|
|
1202
|
+
seenUrls.add(key)
|
|
1203
|
+
return true
|
|
1204
|
+
})
|
|
1066
1205
|
}
|
|
1067
1206
|
} catch (error) {
|
|
1068
1207
|
console.warn("Failed to collect files from drop payload", error)
|
|
1069
1208
|
}
|
|
1070
|
-
if (!files.length) {
|
|
1209
|
+
if (!files.length && (!remoteResources || remoteResources.length === 0)) {
|
|
1071
1210
|
return
|
|
1072
1211
|
}
|
|
1073
|
-
|
|
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
|
+
}
|
|
1074
1222
|
this.term.focus()
|
|
1075
1223
|
})
|
|
1076
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,92 +981,234 @@ 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
|
+
}
|
|
983
1056
|
async collectFilesFromDataTransfer(dataTransfer) {
|
|
984
1057
|
if (!dataTransfer) {
|
|
985
|
-
return []
|
|
1058
|
+
return { urls: [] }
|
|
986
1059
|
}
|
|
987
|
-
const
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const stringValues = await Promise.all(stringItems.map((item) => new Promise((resolve) => {
|
|
992
|
-
try {
|
|
993
|
-
item.getAsString((value) => resolve({ type: item.type || "text/plain", value }))
|
|
994
|
-
} catch (_) {
|
|
995
|
-
resolve({ type: item.type || "text/plain", value: "" })
|
|
996
|
-
}
|
|
997
|
-
})))
|
|
998
|
-
for (const entry of stringValues) {
|
|
999
|
-
if (entry && typeof entry.value === "string" && entry.value.trim()) {
|
|
1000
|
-
collectedStrings.push(entry)
|
|
1001
|
-
}
|
|
1060
|
+
const resourceMap = new Map()
|
|
1061
|
+
const ensureResource = (input, nameHint) => {
|
|
1062
|
+
if (!input) {
|
|
1063
|
+
return
|
|
1002
1064
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
|
1007
1073
|
}
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
|
1011
1081
|
}
|
|
1082
|
+
resourceMap.set(href, { href, url: resolved, nameHint: nameHint || null })
|
|
1012
1083
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
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)
|
|
1015
1097
|
}
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
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()
|
|
1019
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 (_) {
|
|
1020
1154
|
continue
|
|
1021
1155
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1156
|
+
let decodedHref = resolvedHref
|
|
1157
|
+
try {
|
|
1158
|
+
decodedHref = decodeURIComponent(resolvedHref)
|
|
1159
|
+
} catch (_) {}
|
|
1160
|
+
if (/[<>"']/.test(decodedHref)) {
|
|
1024
1161
|
continue
|
|
1025
1162
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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) => {
|
|
1029
1170
|
try {
|
|
1030
|
-
|
|
1171
|
+
item.getAsString((value) => resolve({ type: item.type || "text/plain", value }))
|
|
1031
1172
|
} catch (_) {
|
|
1032
|
-
|
|
1173
|
+
resolve(null)
|
|
1033
1174
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1175
|
+
})))
|
|
1176
|
+
for (const entry of stringValues) {
|
|
1177
|
+
if (entry) {
|
|
1178
|
+
handleStringEntry(entry.type, entry.value)
|
|
1036
1179
|
}
|
|
1037
|
-
urls.add(resolved.href)
|
|
1038
1180
|
}
|
|
1039
1181
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const FileCtor = typeof window !== "undefined" ? window.File : null
|
|
1044
|
-
if (typeof FileCtor !== "function") {
|
|
1045
|
-
return []
|
|
1046
|
-
}
|
|
1047
|
-
const files = []
|
|
1048
|
-
for (const href of urls) {
|
|
1049
|
-
try {
|
|
1050
|
-
const response = await fetch(href, { credentials: "include" })
|
|
1051
|
-
if (!response || !response.ok) {
|
|
1052
|
-
continue
|
|
1053
|
-
}
|
|
1054
|
-
const blob = await response.blob()
|
|
1055
|
-
const nameSegment = href.split("/").pop() || "download"
|
|
1056
|
-
let filename = nameSegment || "download"
|
|
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) {
|
|
1057
1185
|
try {
|
|
1058
|
-
|
|
1186
|
+
const value = dataTransfer.getData(type)
|
|
1187
|
+
if (value) {
|
|
1188
|
+
handleStringEntry(type, value)
|
|
1189
|
+
}
|
|
1059
1190
|
} catch (_) {}
|
|
1060
|
-
if (!filename) {
|
|
1061
|
-
filename = "download"
|
|
1062
|
-
}
|
|
1063
|
-
files.push(new FileCtor([blob], filename, { type: blob.type || "application/octet-stream", lastModified: Date.now() }))
|
|
1064
|
-
} catch (error) {
|
|
1065
|
-
console.warn("Failed to resolve dropped resource", href, error)
|
|
1066
1191
|
}
|
|
1067
1192
|
}
|
|
1068
|
-
|
|
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 }
|
|
1069
1212
|
}
|
|
1070
1213
|
async createTerm (_theme) {
|
|
1071
1214
|
if (!this.term) {
|
|
@@ -1136,25 +1279,43 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
1136
1279
|
dragDepth = 0
|
|
1137
1280
|
dropOverlay.classList.remove("active")
|
|
1138
1281
|
const files = Array.from(event.dataTransfer ? event.dataTransfer.files || [] : [])
|
|
1282
|
+
let remoteResources = []
|
|
1139
1283
|
try {
|
|
1140
1284
|
const extra = await this.collectFilesFromDataTransfer(event.dataTransfer)
|
|
1141
|
-
if (Array.isArray(extra) && extra.length) {
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
seen.add(key)
|
|
1147
|
-
files.push(file)
|
|
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
|
|
1148
1290
|
}
|
|
1149
|
-
|
|
1291
|
+
const key = item.href.trim()
|
|
1292
|
+
if (!key || seenUrls.has(key)) {
|
|
1293
|
+
return false
|
|
1294
|
+
}
|
|
1295
|
+
seenUrls.add(key)
|
|
1296
|
+
return true
|
|
1297
|
+
})
|
|
1150
1298
|
}
|
|
1151
1299
|
} catch (error) {
|
|
1152
1300
|
console.warn("Failed to collect files from drop payload", error)
|
|
1153
1301
|
}
|
|
1154
|
-
if (!files.length) {
|
|
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
|
+
})
|
|
1155
1307
|
return
|
|
1156
1308
|
}
|
|
1157
|
-
|
|
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
|
+
}
|
|
1158
1319
|
this.term.focus()
|
|
1159
1320
|
})
|
|
1160
1321
|
term.attachCustomKeyEventHandler(event => {
|