@yiln-dsh/dsh-plugin-file-explorer 0.4.0 → 0.4.1
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/README.md +16 -15
- package/client.js +230 -7
- package/index.js +24 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-plugin-file-explorer
|
|
1
|
+
# @yiln-dsh/dsh-plugin-file-explorer
|
|
2
2
|
|
|
3
3
|
A DSH `dsh.bundle` that replaces the built-in **details** column of the Web
|
|
4
4
|
GUI with a workspace file explorer.
|
|
@@ -9,7 +9,8 @@ GUI with a workspace file explorer.
|
|
|
9
9
|
- **Files** tab: lists the active session's workspace directory, with folder
|
|
10
10
|
navigation, editable path input, parent/refresh buttons, file and folder
|
|
11
11
|
icons, and file sizes. File rows reveal view / download / delete buttons on
|
|
12
|
-
hover; delete uses a second-click confirm.
|
|
12
|
+
hover; delete uses a second-click confirm. Image View supports fullscreen,
|
|
13
|
+
25%-400% zoom, wheel zoom, and drag-to-pan after zooming.
|
|
13
14
|
- **Git Graph** tab: a vscode/le-git-graph style commit graph rendered from
|
|
14
15
|
the repository containing the current directory —
|
|
15
16
|
- colored lane graph with commit dots and merge curves (all branches or the
|
|
@@ -27,13 +28,12 @@ GUI with a workspace file explorer.
|
|
|
27
28
|
the plugin captures the shell layout store actions when the root entry wires
|
|
28
29
|
them, restores the saved width right after `openDetails()`, and saves the
|
|
29
30
|
column width after every resizer drag.
|
|
30
|
-
- **Collapse to icon bar**:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
panel state is preserved.
|
|
31
|
+
- **Collapse to icon bar**: the collapse button matches the DSH session
|
|
32
|
+
menu's circular panel-toggle style, with the direction mirrored for the
|
|
33
|
+
right-side column. Clicking it keeps a real 56px rail in the layout with
|
|
34
|
+
**Files** and **Git Graph** icons; clicking either reopens that tab at the
|
|
35
|
+
last chosen width. The rail is not a floating overlay, and the panel state
|
|
36
|
+
is preserved.
|
|
37
37
|
|
|
38
38
|
## Layout
|
|
39
39
|
|
|
@@ -45,7 +45,7 @@ GUI with a workspace file explorer.
|
|
|
45
45
|
|
|
46
46
|
## Install
|
|
47
47
|
|
|
48
|
-
The
|
|
48
|
+
The package version is `@yiln-dsh/dsh-plugin-file-explorer@0.4.1`.
|
|
49
49
|
|
|
50
50
|
### Local source directory
|
|
51
51
|
|
|
@@ -61,18 +61,18 @@ pnpm pack
|
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
dsh plugin --profile web add ./dsh-plugin-file-explorer-0.
|
|
64
|
+
dsh plugin --profile web add ./yiln-dsh-dsh-plugin-file-explorer-0.4.1.tgz
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
### npm package
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
cd /path/to/dsh-plugin-file-explorer
|
|
71
|
-
npm publish
|
|
71
|
+
npm publish --access public
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
dsh plugin --profile web add dsh-plugin-file-explorer
|
|
75
|
+
dsh plugin --profile web add @yiln-dsh/dsh-plugin-file-explorer@latest
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
The profile must be the `web` profile. The host row serves the API routes and
|
|
@@ -83,8 +83,9 @@ apply the new profile composition.
|
|
|
83
83
|
|
|
84
84
|
- The Host falls back to `sandboxPolicy.workspaceRoot` when no path is passed,
|
|
85
85
|
and returns `{ ok, path, parent, entries }` JSON — never live objects.
|
|
86
|
-
- `read` previews up to
|
|
87
|
-
|
|
86
|
+
- `read` previews up to 1 MiB of UTF-8 text or 16 MiB of recognized images;
|
|
87
|
+
image previews return a data URL for inline rendering. `download` returns a
|
|
88
|
+
base64 data URL (64 MiB cap) that the browser triggers with `<a download>`.
|
|
88
89
|
- `delete` removes regular files (`rm -f`) and directories recursively
|
|
89
90
|
(`rm -rf`); both are called only after a second-click confirm in the UI.
|
|
90
91
|
- Git routes are read-only. They resolve the repository root with
|
package/client.js
CHANGED
|
@@ -298,6 +298,14 @@ window.__ModuleLoader__.load({
|
|
|
298
298
|
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.22);
|
|
299
299
|
overflow: hidden;
|
|
300
300
|
}
|
|
301
|
+
.dsh-fe-modal-fullscreen {
|
|
302
|
+
width: 100vw;
|
|
303
|
+
height: 100vh;
|
|
304
|
+
max-width: none;
|
|
305
|
+
max-height: none;
|
|
306
|
+
border: 0;
|
|
307
|
+
border-radius: 0;
|
|
308
|
+
}
|
|
301
309
|
.dsh-fe-modal-head {
|
|
302
310
|
display: flex;
|
|
303
311
|
align-items: center;
|
|
@@ -319,6 +327,29 @@ window.__ModuleLoader__.load({
|
|
|
319
327
|
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
320
328
|
font-size: 12px;
|
|
321
329
|
}
|
|
330
|
+
.dsh-fe-image-tools {
|
|
331
|
+
flex: 0 0 auto;
|
|
332
|
+
display: inline-flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
gap: 2px;
|
|
335
|
+
}
|
|
336
|
+
.dsh-fe-image-tool {
|
|
337
|
+
width: 28px;
|
|
338
|
+
height: 28px;
|
|
339
|
+
padding: 0;
|
|
340
|
+
}
|
|
341
|
+
.dsh-fe-image-tool:disabled {
|
|
342
|
+
opacity: 0.35;
|
|
343
|
+
cursor: default;
|
|
344
|
+
}
|
|
345
|
+
.dsh-fe-image-zoom-label {
|
|
346
|
+
display: inline-block;
|
|
347
|
+
min-width: 40px;
|
|
348
|
+
color: var(--dsw-alias-label-secondary, #6b7280);
|
|
349
|
+
font-size: 11px;
|
|
350
|
+
text-align: center;
|
|
351
|
+
white-space: nowrap;
|
|
352
|
+
}
|
|
322
353
|
.dsh-fe-modal-body {
|
|
323
354
|
flex: 1 1 auto;
|
|
324
355
|
min-height: 0;
|
|
@@ -331,6 +362,47 @@ window.__ModuleLoader__.load({
|
|
|
331
362
|
font-size: 12px;
|
|
332
363
|
line-height: 18px;
|
|
333
364
|
}
|
|
365
|
+
.dsh-fe-modal-body-image {
|
|
366
|
+
display: flex;
|
|
367
|
+
align-items: stretch;
|
|
368
|
+
justify-content: stretch;
|
|
369
|
+
min-height: 160px;
|
|
370
|
+
padding: 0;
|
|
371
|
+
overflow: hidden;
|
|
372
|
+
background: var(--dsw-alias-bg-base, #f5f5f4);
|
|
373
|
+
white-space: normal;
|
|
374
|
+
}
|
|
375
|
+
.dsh-fe-image-stage {
|
|
376
|
+
flex: 1 1 auto;
|
|
377
|
+
min-width: 0;
|
|
378
|
+
min-height: 0;
|
|
379
|
+
display: flex;
|
|
380
|
+
align-items: center;
|
|
381
|
+
justify-content: center;
|
|
382
|
+
overflow: hidden;
|
|
383
|
+
touch-action: none;
|
|
384
|
+
}
|
|
385
|
+
.dsh-fe-preview-image {
|
|
386
|
+
display: block;
|
|
387
|
+
max-width: 100%;
|
|
388
|
+
max-height: 60vh;
|
|
389
|
+
width: auto;
|
|
390
|
+
height: auto;
|
|
391
|
+
object-fit: contain;
|
|
392
|
+
user-select: none;
|
|
393
|
+
-webkit-user-drag: none;
|
|
394
|
+
transition: transform 120ms ease;
|
|
395
|
+
}
|
|
396
|
+
.dsh-fe-modal-fullscreen .dsh-fe-preview-image {
|
|
397
|
+
max-height: calc(100vh - 64px);
|
|
398
|
+
}
|
|
399
|
+
.dsh-fe-preview-image-pannable {
|
|
400
|
+
cursor: grab;
|
|
401
|
+
}
|
|
402
|
+
.dsh-fe-preview-image-dragging {
|
|
403
|
+
cursor: grabbing;
|
|
404
|
+
transition: none;
|
|
405
|
+
}
|
|
334
406
|
.dsh-git-bar {
|
|
335
407
|
display: flex;
|
|
336
408
|
align-items: center;
|
|
@@ -726,6 +798,29 @@ window.__ModuleLoader__.load({
|
|
|
726
798
|
React.createElement('line', { x1: '18', y1: '6', x2: '6', y2: '18' }),
|
|
727
799
|
React.createElement('line', { x1: '6', y1: '6', x2: '18', y2: '18' }),
|
|
728
800
|
)
|
|
801
|
+
const zoomOutIcon = React.createElement(React.Fragment, null,
|
|
802
|
+
React.createElement('circle', { cx: '10.5', cy: '10.5', r: '6.5' }),
|
|
803
|
+
React.createElement('line', { x1: '15.5', y1: '15.5', x2: '21', y2: '21' }),
|
|
804
|
+
React.createElement('line', { x1: '8', y1: '10.5', x2: '13', y2: '10.5' }),
|
|
805
|
+
)
|
|
806
|
+
const zoomInIcon = React.createElement(React.Fragment, null,
|
|
807
|
+
React.createElement('circle', { cx: '10.5', cy: '10.5', r: '6.5' }),
|
|
808
|
+
React.createElement('line', { x1: '15.5', y1: '15.5', x2: '21', y2: '21' }),
|
|
809
|
+
React.createElement('line', { x1: '8', y1: '10.5', x2: '13', y2: '10.5' }),
|
|
810
|
+
React.createElement('line', { x1: '10.5', y1: '8', x2: '10.5', y2: '13' }),
|
|
811
|
+
)
|
|
812
|
+
const fullscreenIcon = React.createElement(React.Fragment, null,
|
|
813
|
+
React.createElement('polyline', { points: '8 3 3 3 3 8' }),
|
|
814
|
+
React.createElement('polyline', { points: '16 3 21 3 21 8' }),
|
|
815
|
+
React.createElement('polyline', { points: '8 21 3 21 3 16' }),
|
|
816
|
+
React.createElement('polyline', { points: '16 21 21 21 21 16' }),
|
|
817
|
+
)
|
|
818
|
+
const fullscreenExitIcon = React.createElement(React.Fragment, null,
|
|
819
|
+
React.createElement('polyline', { points: '9 3 9 9 3 9' }),
|
|
820
|
+
React.createElement('polyline', { points: '15 3 15 9 21 9' }),
|
|
821
|
+
React.createElement('polyline', { points: '9 21 9 15 3 15' }),
|
|
822
|
+
React.createElement('polyline', { points: '15 21 15 15 21 15' }),
|
|
823
|
+
)
|
|
729
824
|
// Mirror of DeepSeek's sidebar IconPanelLeftOutline16 (spine on the
|
|
730
825
|
// right): this panel lives on the right edge, so the glyph mirrors the
|
|
731
826
|
// left sidebar's, signaling collapse into the right-edge icon rail.
|
|
@@ -1263,6 +1358,11 @@ window.__ModuleLoader__.load({
|
|
|
1263
1358
|
const [draftPath, setDraftPath] = React.useState('')
|
|
1264
1359
|
const [preview, setPreview] = React.useState(null)
|
|
1265
1360
|
const [previewLoading, setPreviewLoading] = React.useState(false)
|
|
1361
|
+
const [imageScale, setImageScale] = React.useState(1)
|
|
1362
|
+
const [imageOffset, setImageOffset] = React.useState({ x: 0, y: 0 })
|
|
1363
|
+
const [imageFullscreen, setImageFullscreen] = React.useState(false)
|
|
1364
|
+
const [imageDragging, setImageDragging] = React.useState(false)
|
|
1365
|
+
const imageDrag = React.useRef(null)
|
|
1266
1366
|
const [pendingDelete, setPendingDelete] = React.useState(null)
|
|
1267
1367
|
const [view, setView] = React.useState('files')
|
|
1268
1368
|
const [collapsed, setCollapsed] = React.useState(false)
|
|
@@ -1341,7 +1441,7 @@ window.__ModuleLoader__.load({
|
|
|
1341
1441
|
const collapse = () => {
|
|
1342
1442
|
setCollapsed(true)
|
|
1343
1443
|
railActive = true
|
|
1344
|
-
|
|
1444
|
+
closePreview()
|
|
1345
1445
|
setPendingDelete(null)
|
|
1346
1446
|
setEditingPath(false)
|
|
1347
1447
|
if (layout !== undefined) {
|
|
@@ -1373,8 +1473,67 @@ window.__ModuleLoader__.load({
|
|
|
1373
1473
|
setRequestPath(next)
|
|
1374
1474
|
}
|
|
1375
1475
|
|
|
1476
|
+
const resetImageZoom = () => {
|
|
1477
|
+
imageDrag.current = null
|
|
1478
|
+
setImageDragging(false)
|
|
1479
|
+
setImageScale(1)
|
|
1480
|
+
setImageOffset({ x: 0, y: 0 })
|
|
1481
|
+
}
|
|
1482
|
+
const resetImageView = () => {
|
|
1483
|
+
resetImageZoom()
|
|
1484
|
+
setImageFullscreen(false)
|
|
1485
|
+
}
|
|
1486
|
+
const closePreview = () => {
|
|
1487
|
+
resetImageView()
|
|
1488
|
+
setPreview(null)
|
|
1489
|
+
}
|
|
1490
|
+
const setImageZoom = (value) => {
|
|
1491
|
+
const next = Math.max(0.25, Math.min(4, Math.round(value * 4) / 4))
|
|
1492
|
+
setImageScale(next)
|
|
1493
|
+
if (next <= 1) setImageOffset({ x: 0, y: 0 })
|
|
1494
|
+
}
|
|
1495
|
+
const changeImageZoom = (delta) => setImageZoom(imageScale + delta)
|
|
1496
|
+
const handleImageWheel = (event) => {
|
|
1497
|
+
event.preventDefault()
|
|
1498
|
+
changeImageZoom(event.deltaY < 0 ? 0.25 : -0.25)
|
|
1499
|
+
}
|
|
1500
|
+
const startImageDrag = (event) => {
|
|
1501
|
+
if (imageScale <= 1 || event.button !== 0) return
|
|
1502
|
+
imageDrag.current = {
|
|
1503
|
+
pointerId: event.pointerId,
|
|
1504
|
+
startX: event.clientX,
|
|
1505
|
+
startY: event.clientY,
|
|
1506
|
+
originX: imageOffset.x,
|
|
1507
|
+
originY: imageOffset.y,
|
|
1508
|
+
}
|
|
1509
|
+
try { event.currentTarget.setPointerCapture(event.pointerId) } catch (_e) { }
|
|
1510
|
+
setImageDragging(true)
|
|
1511
|
+
}
|
|
1512
|
+
const moveImageDrag = (event) => {
|
|
1513
|
+
const drag = imageDrag.current
|
|
1514
|
+
if (!drag || drag.pointerId !== event.pointerId) return
|
|
1515
|
+
setImageOffset({
|
|
1516
|
+
x: drag.originX + event.clientX - drag.startX,
|
|
1517
|
+
y: drag.originY + event.clientY - drag.startY,
|
|
1518
|
+
})
|
|
1519
|
+
}
|
|
1520
|
+
const endImageDrag = (event) => {
|
|
1521
|
+
const drag = imageDrag.current
|
|
1522
|
+
if (!drag || drag.pointerId !== event.pointerId) return
|
|
1523
|
+
imageDrag.current = null
|
|
1524
|
+
setImageDragging(false)
|
|
1525
|
+
try { event.currentTarget.releasePointerCapture(event.pointerId) } catch (_e) { }
|
|
1526
|
+
}
|
|
1527
|
+
const handlePreviewKeyDown = (event) => {
|
|
1528
|
+
if (event.key !== 'Escape') return
|
|
1529
|
+
event.stopPropagation()
|
|
1530
|
+
if (imageFullscreen) setImageFullscreen(false)
|
|
1531
|
+
else closePreview()
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1376
1534
|
const openPreview = (entry) => {
|
|
1377
1535
|
setPendingDelete(null)
|
|
1536
|
+
resetImageView()
|
|
1378
1537
|
setPreviewLoading(true)
|
|
1379
1538
|
setPreview(null)
|
|
1380
1539
|
api('read', { path: entry.path })
|
|
@@ -1582,30 +1741,94 @@ window.__ModuleLoader__.load({
|
|
|
1582
1741
|
preview &&
|
|
1583
1742
|
React.createElement('div', {
|
|
1584
1743
|
className: 'dsh-fe-overlay',
|
|
1585
|
-
onClick:
|
|
1744
|
+
onClick: closePreview,
|
|
1586
1745
|
},
|
|
1587
1746
|
React.createElement('div', {
|
|
1588
|
-
className: 'dsh-fe-modal',
|
|
1747
|
+
className: preview.kind === 'image' && imageFullscreen ? 'dsh-fe-modal dsh-fe-modal-fullscreen' : 'dsh-fe-modal',
|
|
1589
1748
|
role: 'dialog',
|
|
1590
1749
|
'aria-label': 'File preview',
|
|
1750
|
+
'aria-modal': true,
|
|
1751
|
+
tabIndex: -1,
|
|
1752
|
+
onKeyDown: handlePreviewKeyDown,
|
|
1591
1753
|
onClick: (event) => event.stopPropagation(),
|
|
1592
1754
|
},
|
|
1593
1755
|
React.createElement('div', { className: 'dsh-fe-modal-head' },
|
|
1594
1756
|
React.createElement('div', { className: 'dsh-fe-modal-title' }, preview.name || 'Preview'),
|
|
1595
|
-
React.createElement('div', { className: 'dsh-fe-modal-meta' }, preview.binary ? 'Binary' : formatSize(preview.size)),
|
|
1757
|
+
React.createElement('div', { className: 'dsh-fe-modal-meta' }, preview.kind === 'image' ? `Image / ${formatSize(preview.size)}` : preview.binary ? 'Binary' : formatSize(preview.size)),
|
|
1758
|
+
preview.kind === 'image' && preview.dataUrl
|
|
1759
|
+
? React.createElement('div', {
|
|
1760
|
+
className: 'dsh-fe-image-tools',
|
|
1761
|
+
role: 'toolbar',
|
|
1762
|
+
'aria-label': 'Image controls',
|
|
1763
|
+
},
|
|
1764
|
+
React.createElement('button', {
|
|
1765
|
+
type: 'button',
|
|
1766
|
+
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1767
|
+
disabled: imageScale <= 0.25,
|
|
1768
|
+
onClick: () => changeImageZoom(-0.25),
|
|
1769
|
+
'aria-label': 'Zoom out',
|
|
1770
|
+
title: 'Zoom out',
|
|
1771
|
+
}, svgIcon(zoomOutIcon, 14)),
|
|
1772
|
+
React.createElement('button', {
|
|
1773
|
+
type: 'button',
|
|
1774
|
+
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1775
|
+
onClick: resetImageZoom,
|
|
1776
|
+
'aria-label': 'Reset image zoom',
|
|
1777
|
+
title: 'Reset zoom',
|
|
1778
|
+
}, React.createElement('span', { className: 'dsh-fe-image-zoom-label' }, `${Math.round(imageScale * 100)}%`)),
|
|
1779
|
+
React.createElement('button', {
|
|
1780
|
+
type: 'button',
|
|
1781
|
+
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1782
|
+
disabled: imageScale >= 4,
|
|
1783
|
+
onClick: () => changeImageZoom(0.25),
|
|
1784
|
+
'aria-label': 'Zoom in',
|
|
1785
|
+
title: 'Zoom in',
|
|
1786
|
+
}, svgIcon(zoomInIcon, 14)),
|
|
1787
|
+
React.createElement('button', {
|
|
1788
|
+
type: 'button',
|
|
1789
|
+
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1790
|
+
onClick: () => setImageFullscreen((value) => !value),
|
|
1791
|
+
'aria-label': imageFullscreen ? 'Exit fullscreen' : 'Enter fullscreen',
|
|
1792
|
+
title: imageFullscreen ? 'Exit fullscreen' : 'Fullscreen',
|
|
1793
|
+
}, svgIcon(imageFullscreen ? fullscreenExitIcon : fullscreenIcon, 14)),
|
|
1794
|
+
)
|
|
1795
|
+
: null,
|
|
1596
1796
|
React.createElement('button', {
|
|
1597
1797
|
type: 'button',
|
|
1598
1798
|
className: 'dsh-fe-icon',
|
|
1599
|
-
onClick:
|
|
1799
|
+
onClick: closePreview,
|
|
1600
1800
|
'aria-label': 'Close preview',
|
|
1601
1801
|
}, svgIcon(closeIcon, 14)),
|
|
1602
1802
|
),
|
|
1603
|
-
React.createElement('div', {
|
|
1803
|
+
React.createElement('div', {
|
|
1804
|
+
className: preview.kind === 'image' ? 'dsh-fe-modal-body dsh-fe-modal-body-image' : 'dsh-fe-modal-body',
|
|
1805
|
+
onWheel: preview.kind === 'image' && preview.dataUrl ? handleImageWheel : undefined,
|
|
1806
|
+
onPointerDown: preview.kind === 'image' && preview.dataUrl ? startImageDrag : undefined,
|
|
1807
|
+
onPointerMove: preview.kind === 'image' && preview.dataUrl ? moveImageDrag : undefined,
|
|
1808
|
+
onPointerUp: preview.kind === 'image' && preview.dataUrl ? endImageDrag : undefined,
|
|
1809
|
+
onPointerCancel: preview.kind === 'image' && preview.dataUrl ? endImageDrag : undefined,
|
|
1810
|
+
},
|
|
1604
1811
|
previewLoading
|
|
1605
1812
|
? 'Loading...'
|
|
1606
1813
|
: preview.error
|
|
1607
1814
|
? preview.error
|
|
1608
|
-
: preview.
|
|
1815
|
+
: preview.kind === 'image'
|
|
1816
|
+
? preview.dataUrl
|
|
1817
|
+
? React.createElement('div', { className: 'dsh-fe-image-stage' },
|
|
1818
|
+
React.createElement('img', {
|
|
1819
|
+
className: 'dsh-fe-preview-image' + (imageScale > 1 ? ' dsh-fe-preview-image-pannable' : '') + (imageDragging ? ' dsh-fe-preview-image-dragging' : ''),
|
|
1820
|
+
style: { transform: `translate3d(${imageOffset.x}px, ${imageOffset.y}px, 0) scale(${imageScale})` },
|
|
1821
|
+
src: preview.dataUrl,
|
|
1822
|
+
alt: preview.name || 'Image preview',
|
|
1823
|
+
draggable: false,
|
|
1824
|
+
})
|
|
1825
|
+
)
|
|
1826
|
+
: preview.tooLarge ? 'Image is too large to preview here' : '(No preview)'
|
|
1827
|
+
: preview.text !== undefined && preview.text !== null
|
|
1828
|
+
? preview.text
|
|
1829
|
+
: preview.tooLarge
|
|
1830
|
+
? 'File is too large to preview here'
|
|
1831
|
+
: preview.binary ? 'Binary file cannot be previewed' : '(No preview)',
|
|
1609
1832
|
),
|
|
1610
1833
|
),
|
|
1611
1834
|
),
|
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* dsh-plugin-file-explorer — Host half (static DSH bundle).
|
|
3
3
|
*
|
|
4
4
|
* Registers exact HTTP routes under /_dsh/file-explorer for the browser
|
|
5
|
-
* bundle: list (with parent path), read (text preview), download (data URL),
|
|
5
|
+
* bundle: list (with parent path), read (text/image preview), download (data URL),
|
|
6
6
|
* and delete.
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -145,12 +145,32 @@ export function apply(ctx) {
|
|
|
145
145
|
const target = await fs.resolve(req.path)
|
|
146
146
|
const info = await fs.stat(target)
|
|
147
147
|
const size = info && typeof info.size === 'number' ? info.size : null
|
|
148
|
-
const
|
|
148
|
+
const name = target.displayPath.split('/').pop()
|
|
149
|
+
const mime = guessMime(name)
|
|
150
|
+
const isImage = mime.startsWith('image/')
|
|
151
|
+
const limit = isImage ? 16 * 1024 * 1024 : 1024 * 1024
|
|
149
152
|
if (size !== null && size > limit) {
|
|
150
|
-
sendJson(res, 200, {
|
|
153
|
+
sendJson(res, 200, {
|
|
154
|
+
ok: true,
|
|
155
|
+
name,
|
|
156
|
+
size,
|
|
157
|
+
tooLarge: true,
|
|
158
|
+
...(isImage ? { kind: 'image', mime } : {}),
|
|
159
|
+
})
|
|
151
160
|
return
|
|
152
161
|
}
|
|
153
162
|
const bytes = await fs.readBytes(target, undefined, limit)
|
|
163
|
+
if (isImage) {
|
|
164
|
+
sendJson(res, 200, {
|
|
165
|
+
ok: true,
|
|
166
|
+
name,
|
|
167
|
+
size,
|
|
168
|
+
kind: 'image',
|
|
169
|
+
mime,
|
|
170
|
+
dataUrl: `data:${mime};base64,${bytesToBase64(bytes)}`,
|
|
171
|
+
})
|
|
172
|
+
return
|
|
173
|
+
}
|
|
154
174
|
const text = new TextDecoder('utf-8', { fatal: false }).decode(bytes)
|
|
155
175
|
let binary = false
|
|
156
176
|
for (let i = 0; i < bytes.length; i += 1) {
|
|
@@ -159,7 +179,7 @@ export function apply(ctx) {
|
|
|
159
179
|
if (!binary && text.replace(/\uFFFD/g, '').length * 10 < text.length * 9) binary = true
|
|
160
180
|
sendJson(res, 200, {
|
|
161
181
|
ok: true,
|
|
162
|
-
name
|
|
182
|
+
name,
|
|
163
183
|
size,
|
|
164
184
|
text: binary ? null : text,
|
|
165
185
|
binary,
|