@yiln-dsh/dsh-plugin-file-explorer 0.4.1 → 0.5.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 +18 -27
- package/client.js +325 -350
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# @yiln-dsh/dsh-plugin-file-explorer
|
|
2
2
|
|
|
3
|
-
A DSH `dsh.bundle` that
|
|
4
|
-
|
|
3
|
+
A DSH `dsh.bundle` that contributes a workspace file explorer page to the
|
|
4
|
+
`@yiln-dsh/dsh-plugin-right-panel` host.
|
|
5
5
|
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Files**
|
|
6
|
+
- Registers two independent right-panel pages: **Files** and **Git Graph**. The
|
|
7
|
+
host owns the right-column layout, navigation rail, width, and collapse
|
|
8
|
+
behavior.
|
|
9
|
+
- **Files** page: 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
12
|
hover; delete uses a second-click confirm. Image View supports fullscreen,
|
|
13
13
|
25%-400% zoom, wheel zoom, and drag-to-pan after zooming.
|
|
14
|
-
- **Git Graph**
|
|
14
|
+
- **Git Graph** page: a vscode/le-git-graph style commit graph rendered from
|
|
15
15
|
the repository containing the current directory —
|
|
16
16
|
- colored lane graph with commit dots and merge curves (all branches or the
|
|
17
17
|
current branch, refreshable),
|
|
@@ -22,34 +22,26 @@ GUI with a workspace file explorer.
|
|
|
22
22
|
A/M/D/R status badges,
|
|
23
23
|
- click a file to open its diff patch (commit shows `git show`, working tree
|
|
24
24
|
shows `git diff HEAD`) in a dialog.
|
|
25
|
-
- The
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
the plugin captures the shell layout store actions when the root entry wires
|
|
29
|
-
them, restores the saved width right after `openDetails()`, and saves the
|
|
30
|
-
column width after every resizer drag.
|
|
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.
|
|
25
|
+
- The host keeps a real 56px right-edge icon rail when collapsed. Other
|
|
26
|
+
right-panel pages can appear alongside Files and overflow into the host's
|
|
27
|
+
searchable More menu.
|
|
37
28
|
|
|
38
29
|
## Layout
|
|
39
30
|
|
|
40
31
|
| File | Content |
|
|
41
32
|
| --- | --- |
|
|
42
33
|
| `index.js` | Host half: registers exact `/ _dsh/file-explorer/*` routes backed by the Host `fs`/`subprocess` services, plus read-only git routes (`/ _dsh/file-explorer/git-log`, `git-commit`, `git-diff`, `git-status`) that run `git` with machine-readable separators and return JSON only. |
|
|
43
|
-
| `client.js` | Client half:
|
|
34
|
+
| `client.js` | Client half: registers independent keyed `right-panel.page` entries for Files and Git Graph. |
|
|
44
35
|
| `cordis.patch.yml` | Composition patch that mounts the host row — declared with `inject: [webServer]`, so it activates only after the stock webserver service (`127.0.0.1`) is up. |
|
|
45
36
|
|
|
46
37
|
## Install
|
|
47
38
|
|
|
48
|
-
The package version is `@yiln-dsh/dsh-plugin-file-explorer@0.
|
|
39
|
+
The package version is `@yiln-dsh/dsh-plugin-file-explorer@0.5.1`.
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
The right-panel package must be installed in the same `web` profile:
|
|
51
42
|
|
|
52
43
|
```bash
|
|
44
|
+
dsh plugin --profile web add file:/path/to/dsh-plugin-right-panel
|
|
53
45
|
dsh plugin --profile web add file:/path/to/dsh-plugin-file-explorer
|
|
54
46
|
```
|
|
55
47
|
|
|
@@ -61,7 +53,7 @@ pnpm pack
|
|
|
61
53
|
```
|
|
62
54
|
|
|
63
55
|
```bash
|
|
64
|
-
dsh plugin --profile web add ./yiln-dsh-dsh-plugin-file-explorer-0.
|
|
56
|
+
dsh plugin --profile web add ./yiln-dsh-dsh-plugin-file-explorer-0.5.1.tgz
|
|
65
57
|
```
|
|
66
58
|
|
|
67
59
|
### npm package
|
|
@@ -98,7 +90,6 @@ apply the new profile composition.
|
|
|
98
90
|
sentinel) and file paths against a non-option, non-control-character check.
|
|
99
91
|
- Rows have a fixed 34px height, so the hover action swap never changes the
|
|
100
92
|
row height; directories reveal a delete-only action set on hover.
|
|
101
|
-
- The browser module
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`useSessions` hook, and its own stylesheet.
|
|
93
|
+
- The browser module registers `file-explorer.files` and
|
|
94
|
+
`file-explorer.git` into the keyed `right-panel.page` Slot and uses the
|
|
95
|
+
standard session `useSessions` hook.
|
package/client.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-plugin-file-explorer — browser bundle (static DSH client module).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the conversation and keeps the native resizer handle.
|
|
4
|
+
* Registers the file explorer as a page in the `right-panel` host. The host
|
|
5
|
+
* owns the right layout column, width, navigation rail, and page switching.
|
|
7
6
|
*
|
|
8
|
-
* Two views behind the
|
|
7
|
+
* Two views behind the page's local tabs:
|
|
9
8
|
* - Files: directory browser (list / read / download / delete via the
|
|
10
9
|
* /_dsh/file-explorer API).
|
|
11
10
|
* - Git Graph: a vscode/git-graph style commit DAG with colored lanes, ref
|
|
@@ -20,12 +19,181 @@ window.__ModuleLoader__.load({
|
|
|
20
19
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
21
20
|
let React = require("react");
|
|
22
21
|
|
|
23
|
-
const inject = ["slots", "
|
|
22
|
+
const inject = ["slots", "rightPanel"];
|
|
23
|
+
|
|
24
|
+
const LOCALE_NS = "file-explorer";
|
|
25
|
+
const ZH_DICT = {
|
|
26
|
+
"files.title": "文件",
|
|
27
|
+
"git.title": "Git 图谱",
|
|
28
|
+
"workspace": "工作区",
|
|
29
|
+
"files.refresh": "刷新",
|
|
30
|
+
"files.refreshList": "刷新文件列表",
|
|
31
|
+
"git.refreshGraph": "刷新图谱",
|
|
32
|
+
"files.up": "上一级",
|
|
33
|
+
"files.goUp": "返回上级目录",
|
|
34
|
+
"files.delete": "删除",
|
|
35
|
+
"files.deleteFile": "删除 {name}",
|
|
36
|
+
"files.confirmDelete": "确认删除 {name}",
|
|
37
|
+
"files.clickAgain": "再次点击确认删除",
|
|
38
|
+
"files.deleteFailed": "删除失败",
|
|
39
|
+
"files.view": "查看",
|
|
40
|
+
"files.viewFile": "查看 {name}",
|
|
41
|
+
"files.download": "下载",
|
|
42
|
+
"files.downloadFile": "下载 {name}",
|
|
43
|
+
"files.downloadFailed": "下载失败",
|
|
44
|
+
"files.closePreview": "关闭预览",
|
|
45
|
+
"files.preview": "预览",
|
|
46
|
+
"files.fullscreen": "全屏",
|
|
47
|
+
"files.enterFullscreen": "进入全屏",
|
|
48
|
+
"files.exitFullscreen": "退出全屏",
|
|
49
|
+
"files.imageControls": "图片控制",
|
|
50
|
+
"files.zoomIn": "放大",
|
|
51
|
+
"files.zoomOut": "缩小",
|
|
52
|
+
"files.resetZoom": "重置缩放",
|
|
53
|
+
"files.resetImageZoom": "重置图片缩放",
|
|
54
|
+
"files.imagePreview": "图片预览",
|
|
55
|
+
"files.previewDialog": "文件预览",
|
|
56
|
+
"files.workspaceRoot": "工作区根目录",
|
|
57
|
+
"files.loading": "加载中…",
|
|
58
|
+
"git.loading": "加载中…",
|
|
59
|
+
"files.empty": "空文件夹",
|
|
60
|
+
"files.listFailed": "文件列表加载失败",
|
|
61
|
+
"files.readFailed": "文件读取失败",
|
|
62
|
+
"files.imageMeta": "图片 / {size}",
|
|
63
|
+
"files.binary": "二进制文件",
|
|
64
|
+
"files.fileTooLarge": "文件过大,无法在此预览",
|
|
65
|
+
"files.imageTooLarge": "图片过大,无法在此预览",
|
|
66
|
+
"files.noPreview": "(无预览)",
|
|
67
|
+
"files.binaryNoPreview": "二进制文件无法预览",
|
|
68
|
+
"git.logFailed": "Git 提交记录加载失败",
|
|
69
|
+
"git.commitFailed": "提交详情加载失败",
|
|
70
|
+
"git.diffFailed": "差异加载失败",
|
|
71
|
+
"git.loadingChanges": "加载变更…",
|
|
72
|
+
"git.noDiff": "(无差异)",
|
|
73
|
+
"git.noMessage": "(无提交信息)",
|
|
74
|
+
"git.diffTruncated": "…(差异已截断)",
|
|
75
|
+
"git.uncommitted": "未提交的更改",
|
|
76
|
+
"git.workingTree": "工作区",
|
|
77
|
+
"git.workingTreeLabel": "工作区",
|
|
78
|
+
"git.noCommitsYet": "暂无提交",
|
|
79
|
+
"git.noCommitsYetSuffix": "(暂无提交)",
|
|
80
|
+
"git.noData": "暂无数据",
|
|
81
|
+
"git.detachedHead": "游离 HEAD",
|
|
82
|
+
"git.allBranches": "所有分支",
|
|
83
|
+
"git.branchFilter": "分支筛选",
|
|
84
|
+
"git.currentBranch": "当前分支",
|
|
85
|
+
"git.diff": "差异",
|
|
86
|
+
"git.closeDiff": "关闭差异",
|
|
87
|
+
"git.authorUnknown": "未知",
|
|
88
|
+
"git.changedFiles.one": "共 {count} 个变更文件",
|
|
89
|
+
"git.changedFiles.other": "共 {count} 个变更文件",
|
|
90
|
+
"git.changedVsHead.one": "共 {count} 个变更文件(对比 HEAD)",
|
|
91
|
+
"git.changedVsHead.other": "共 {count} 个变更文件(对比 HEAD)",
|
|
92
|
+
"git.changesCount.one": "{count} 个变更",
|
|
93
|
+
"git.changesCount.other": "{count} 个变更",
|
|
94
|
+
"time.justNow": "刚刚",
|
|
95
|
+
"time.minutesAgo": "{n} 分钟前",
|
|
96
|
+
"time.hoursAgo": "{n} 小时前",
|
|
97
|
+
"time.daysAgo": "{n} 天前",
|
|
98
|
+
"time.weeksAgo": "{n} 周前",
|
|
99
|
+
"time.monthsAgo": "{n} 个月前",
|
|
100
|
+
"time.yearsAgo": "{n} 年前",
|
|
101
|
+
};
|
|
102
|
+
const EN_DICT = {
|
|
103
|
+
"files.title": "Files",
|
|
104
|
+
"git.title": "Git Graph",
|
|
105
|
+
"workspace": "Workspace",
|
|
106
|
+
"files.refresh": "Refresh",
|
|
107
|
+
"files.refreshList": "Refresh file list",
|
|
108
|
+
"git.refreshGraph": "Refresh graph",
|
|
109
|
+
"files.up": "Up",
|
|
110
|
+
"files.goUp": "Go to parent directory",
|
|
111
|
+
"files.delete": "Delete",
|
|
112
|
+
"files.deleteFile": "Delete {name}",
|
|
113
|
+
"files.confirmDelete": "Confirm deleting {name}",
|
|
114
|
+
"files.clickAgain": "Click again to delete",
|
|
115
|
+
"files.deleteFailed": "Delete failed",
|
|
116
|
+
"files.view": "View",
|
|
117
|
+
"files.viewFile": "View {name}",
|
|
118
|
+
"files.download": "Download",
|
|
119
|
+
"files.downloadFile": "Download {name}",
|
|
120
|
+
"files.downloadFailed": "Download failed",
|
|
121
|
+
"files.closePreview": "Close preview",
|
|
122
|
+
"files.preview": "Preview",
|
|
123
|
+
"files.fullscreen": "Fullscreen",
|
|
124
|
+
"files.enterFullscreen": "Enter fullscreen",
|
|
125
|
+
"files.exitFullscreen": "Exit fullscreen",
|
|
126
|
+
"files.imageControls": "Image controls",
|
|
127
|
+
"files.zoomIn": "Zoom in",
|
|
128
|
+
"files.zoomOut": "Zoom out",
|
|
129
|
+
"files.resetZoom": "Reset zoom",
|
|
130
|
+
"files.resetImageZoom": "Reset image zoom",
|
|
131
|
+
"files.imagePreview": "Image preview",
|
|
132
|
+
"files.previewDialog": "File preview",
|
|
133
|
+
"files.workspaceRoot": "workspace root",
|
|
134
|
+
"files.loading": "Loading...",
|
|
135
|
+
"git.loading": "Loading…",
|
|
136
|
+
"files.empty": "Empty folder",
|
|
137
|
+
"files.listFailed": "Failed to list files",
|
|
138
|
+
"files.readFailed": "Failed to read file",
|
|
139
|
+
"files.imageMeta": "Image / {size}",
|
|
140
|
+
"files.binary": "Binary",
|
|
141
|
+
"files.fileTooLarge": "File is too large to preview here",
|
|
142
|
+
"files.imageTooLarge": "Image is too large to preview here",
|
|
143
|
+
"files.noPreview": "(No preview)",
|
|
144
|
+
"files.binaryNoPreview": "Binary file cannot be previewed",
|
|
145
|
+
"git.logFailed": "Failed to load git log",
|
|
146
|
+
"git.commitFailed": "Failed to load commit",
|
|
147
|
+
"git.diffFailed": "Failed to load diff",
|
|
148
|
+
"git.loadingChanges": "Loading changes…",
|
|
149
|
+
"git.noDiff": "(no diff)",
|
|
150
|
+
"git.noMessage": "(no message)",
|
|
151
|
+
"git.diffTruncated": "… (diff truncated)",
|
|
152
|
+
"git.uncommitted": "Uncommitted changes",
|
|
153
|
+
"git.workingTree": "Working tree",
|
|
154
|
+
"git.workingTreeLabel": "working tree",
|
|
155
|
+
"git.noCommitsYet": "No commits yet",
|
|
156
|
+
"git.noCommitsYetSuffix": " (no commits yet)",
|
|
157
|
+
"git.noData": "No data",
|
|
158
|
+
"git.detachedHead": "detached HEAD",
|
|
159
|
+
"git.allBranches": "All branches",
|
|
160
|
+
"git.branchFilter": "Branch filter",
|
|
161
|
+
"git.currentBranch": "Current branch",
|
|
162
|
+
"git.diff": "Diff",
|
|
163
|
+
"git.closeDiff": "Close diff",
|
|
164
|
+
"git.authorUnknown": "unknown",
|
|
165
|
+
"git.changedFiles.one": "{count} changed file",
|
|
166
|
+
"git.changedFiles.other": "{count} changed files",
|
|
167
|
+
"git.changedVsHead.one": "{count} changed file (vs HEAD)",
|
|
168
|
+
"git.changedVsHead.other": "{count} changed files (vs HEAD)",
|
|
169
|
+
"git.changesCount.one": "{count} change",
|
|
170
|
+
"git.changesCount.other": "{count} changes",
|
|
171
|
+
"time.justNow": "just now",
|
|
172
|
+
"time.minutesAgo": "{n}m ago",
|
|
173
|
+
"time.hoursAgo": "{n}h ago",
|
|
174
|
+
"time.daysAgo": "{n}d ago",
|
|
175
|
+
"time.weeksAgo": "{n}w ago",
|
|
176
|
+
"time.monthsAgo": "{n}mo ago",
|
|
177
|
+
"time.yearsAgo": "{n}y ago",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
function applyParams(template, params) {
|
|
181
|
+
if (!params) return template;
|
|
182
|
+
return template.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match);
|
|
183
|
+
}
|
|
24
184
|
|
|
25
185
|
function apply(ctx) {
|
|
26
186
|
const slots = ctx.get('slots')
|
|
27
|
-
const
|
|
28
|
-
if (slots === undefined) return
|
|
187
|
+
const rightPanel = ctx.get('rightPanel')
|
|
188
|
+
if (slots === undefined || rightPanel === undefined) return
|
|
189
|
+
|
|
190
|
+
const locale = ctx.get("locale");
|
|
191
|
+
if (locale !== undefined) {
|
|
192
|
+
ctx.effect(() => locale.register(LOCALE_NS, { zh: ZH_DICT, en: EN_DICT }), "file-explorer: locale");
|
|
193
|
+
}
|
|
194
|
+
const t = locale !== undefined
|
|
195
|
+
? locale.bind(LOCALE_NS)
|
|
196
|
+
: (key, params) => applyParams(ZH_DICT[key] ?? EN_DICT[key] ?? key, params);
|
|
29
197
|
|
|
30
198
|
const style = document.createElement("style");
|
|
31
199
|
style.id = "dsh-plugin-file-explorer-style";
|
|
@@ -77,35 +245,6 @@ window.__ModuleLoader__.load({
|
|
|
77
245
|
.dsh-fe-icon:hover {
|
|
78
246
|
background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2, #e7e5e4));
|
|
79
247
|
}
|
|
80
|
-
.dsh-fe-tabs {
|
|
81
|
-
display: flex;
|
|
82
|
-
gap: 2px;
|
|
83
|
-
padding: 8px 12px 0;
|
|
84
|
-
}
|
|
85
|
-
.dsh-fe-tab {
|
|
86
|
-
flex: 1 1 0;
|
|
87
|
-
height: 26px;
|
|
88
|
-
border: 0;
|
|
89
|
-
border-radius: 6px;
|
|
90
|
-
background: transparent;
|
|
91
|
-
color: var(--dsw-alias-label-secondary, #4b5563);
|
|
92
|
-
cursor: pointer;
|
|
93
|
-
font: inherit;
|
|
94
|
-
font-size: 12px;
|
|
95
|
-
display: inline-flex;
|
|
96
|
-
align-items: center;
|
|
97
|
-
justify-content: center;
|
|
98
|
-
gap: 6px;
|
|
99
|
-
}
|
|
100
|
-
.dsh-fe-tab:hover {
|
|
101
|
-
background: var(--dsw-alias-bg-layer-2, #e7e5e4);
|
|
102
|
-
}
|
|
103
|
-
.dsh-fe-tab-active,
|
|
104
|
-
.dsh-fe-tab-active:hover {
|
|
105
|
-
background: var(--dsw-alias-bg-layer-2, #e7e5e4);
|
|
106
|
-
color: var(--dsw-alias-label-primary, #111827);
|
|
107
|
-
font-weight: 600;
|
|
108
|
-
}
|
|
109
248
|
.dsh-fe-path {
|
|
110
249
|
min-width: 0;
|
|
111
250
|
margin: 8px 12px 0;
|
|
@@ -587,154 +726,19 @@ window.__ModuleLoader__.load({
|
|
|
587
726
|
font-size: 12px;
|
|
588
727
|
unicode-bidi: plaintext;
|
|
589
728
|
}
|
|
590
|
-
.dsh-fe-
|
|
729
|
+
.dsh-fe-page {
|
|
591
730
|
height: 100%;
|
|
592
|
-
|
|
593
|
-
box-sizing: border-box;
|
|
731
|
+
min-height: 0;
|
|
594
732
|
display: flex;
|
|
595
733
|
flex-direction: column;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
padding: 10px 6px;
|
|
734
|
+
box-sizing: border-box;
|
|
735
|
+
overflow: hidden;
|
|
599
736
|
background: var(--dsw-specific-sidebar-fill, var(--dsw-alias-bg-base, #f5f5f4));
|
|
600
|
-
|
|
601
|
-
}
|
|
602
|
-
/* While collapsed, keep a real RAIL_WIDTH column in the layout (like the left
|
|
603
|
-
sidebar's 56px rail) instead of a floating overlay. This !important rule
|
|
604
|
-
overrides React's inline grid-template-columns on the app frame; the data
|
|
605
|
-
attribute is added by applyRail() and removed on expand/unload. */
|
|
606
|
-
[data-dsh-fe-rail] {
|
|
607
|
-
grid-template-columns: var(--dsh-fe-rail-sidebar, 280px) minmax(0, 1fr) 56px !important;
|
|
608
|
-
}
|
|
609
|
-
.dsh-fe-rail-button {
|
|
610
|
-
width: 36px;
|
|
611
|
-
height: 36px;
|
|
612
|
-
border: 0;
|
|
613
|
-
border-radius: 8px;
|
|
614
|
-
background: transparent;
|
|
615
|
-
color: var(--dsw-alias-label-secondary, #4b5563);
|
|
616
|
-
cursor: pointer;
|
|
617
|
-
display: inline-flex;
|
|
618
|
-
align-items: center;
|
|
619
|
-
justify-content: center;
|
|
620
|
-
}
|
|
621
|
-
.dsh-fe-rail-button:hover {
|
|
622
|
-
background: var(--dsw-alias-bg-layer-2, #e7e5e4);
|
|
623
|
-
color: var(--dsw-alias-brand-primary, #4f46e5);
|
|
737
|
+
color: var(--dsw-alias-label-primary, #111827);
|
|
624
738
|
}
|
|
625
739
|
`;
|
|
626
740
|
document.head.append(style);
|
|
627
741
|
|
|
628
|
-
// --- details-column width persistence ---------------------------------
|
|
629
|
-
// The shell's layout store is transient: openDetails() resets the column
|
|
630
|
-
// to its 360px contract default on every mount and session switch. The
|
|
631
|
-
// bound store actions reach the LayoutController after every client
|
|
632
|
-
// module has applied (the root entry's inject hook fires on first
|
|
633
|
-
// render and again on re-register), so wrapping attachPanels on the
|
|
634
|
-
// prototype captures them reliably. setDetails then restores the last
|
|
635
|
-
// user-chosen width, and a document-level pointerup listener persists
|
|
636
|
-
// the column width after each resizer drag.
|
|
637
|
-
const WIDTH_KEY = "dsh-plugin-file-explorer.detailsWidth";
|
|
638
|
-
const WIDTH_MIN = 300; // the layout contract's clamp floor
|
|
639
|
-
let panelActions = null;
|
|
640
|
-
let savedWidth = 0;
|
|
641
|
-
try { savedWidth = Number(window.localStorage.getItem(WIDTH_KEY)) || 0 } catch (_e) { }
|
|
642
|
-
|
|
643
|
-
const findDetailsColumn = () => {
|
|
644
|
-
// The details column's own class is the reliable anchor: the drag
|
|
645
|
-
// handle's previousElementSibling is the sidebar handle or the
|
|
646
|
-
// overlay layer, never the column itself.
|
|
647
|
-
const byClass = document.querySelector('[class*="detailsCol"]')
|
|
648
|
-
if (byClass !== null) return byClass
|
|
649
|
-
const handle = document.querySelector('[data-side="details"]')
|
|
650
|
-
if (handle !== null && handle.previousElementSibling !== null) return handle.previousElementSibling
|
|
651
|
-
return null
|
|
652
|
-
}
|
|
653
|
-
const persistWidth = () => {
|
|
654
|
-
const column = findDetailsColumn()
|
|
655
|
-
if (column === null) return
|
|
656
|
-
const width = Math.round(column.getBoundingClientRect().width)
|
|
657
|
-
if (width < WIDTH_MIN) return
|
|
658
|
-
savedWidth = width
|
|
659
|
-
try { window.localStorage.setItem(WIDTH_KEY, String(width)) } catch (_e) { }
|
|
660
|
-
}
|
|
661
|
-
const onDocumentPointerUp = () => { persistWidth() }
|
|
662
|
-
document.addEventListener("pointerup", onDocumentPointerUp, true)
|
|
663
|
-
ctx.effect(() => () => document.removeEventListener("pointerup", onDocumentPointerUp, true))
|
|
664
|
-
|
|
665
|
-
if (layout !== undefined) {
|
|
666
|
-
const proto = Object.getPrototypeOf(layout)
|
|
667
|
-
if (proto !== null && typeof proto.attachPanels === "function") {
|
|
668
|
-
const originalAttach = proto.attachPanels
|
|
669
|
-
proto.attachPanels = function (actions) {
|
|
670
|
-
panelActions = actions
|
|
671
|
-
return originalAttach.call(this, actions)
|
|
672
|
-
}
|
|
673
|
-
ctx.effect(() => () => { proto.attachPanels = originalAttach })
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
const restoreWidth = () => {
|
|
677
|
-
if (savedWidth < WIDTH_MIN) return
|
|
678
|
-
if (panelActions === null || typeof panelActions.setDetails !== "function") return
|
|
679
|
-
try { panelActions.setDetails(savedWidth) } catch (_e) { }
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// --- collapsed rail as a real layout column --------------------------
|
|
683
|
-
// The layout contract gives the sidebar a 56px rail when closed but
|
|
684
|
-
// closes the details column to 0px (no rail). To collapse like the left
|
|
685
|
-
// sidebar, keep the store closed (no drag handle / border) and force the
|
|
686
|
-
// frame's third grid track to RAIL_WIDTH via an !important stylesheet
|
|
687
|
-
// rule keyed on a data attribute we own. CSS !important beats React's
|
|
688
|
-
// inline grid style, so there is no fight with React re-renders: set the
|
|
689
|
-
// attribute to collapse, remove it to expand. A MutationObserver only
|
|
690
|
-
// refreshes the sidebar-width variable (the first track) when the app
|
|
691
|
-
// re-renders the frame.
|
|
692
|
-
const RAIL_WIDTH = 56;
|
|
693
|
-
let railActive = false;
|
|
694
|
-
const frameOfDetails = () => {
|
|
695
|
-
const column = findDetailsColumn();
|
|
696
|
-
if (column === null || column.parentElement === null) return null;
|
|
697
|
-
return column.parentElement;
|
|
698
|
-
};
|
|
699
|
-
const syncRailVar = () => {
|
|
700
|
-
const frame = frameOfDetails();
|
|
701
|
-
if (frame === null) return;
|
|
702
|
-
const sideCol = frame.children.length > 0 ? frame.children[0] : null;
|
|
703
|
-
const sidebarPx = sideCol !== null ? Math.round(sideCol.getBoundingClientRect().width) : 280;
|
|
704
|
-
try { frame.style.setProperty("--dsh-fe-rail-sidebar", `${sidebarPx}px`) } catch (_e) { }
|
|
705
|
-
};
|
|
706
|
-
const applyRail = () => {
|
|
707
|
-
const frame = frameOfDetails();
|
|
708
|
-
if (frame === null) return;
|
|
709
|
-
if (railActive) {
|
|
710
|
-
frame.setAttribute("data-dsh-fe-rail", "");
|
|
711
|
-
syncRailVar();
|
|
712
|
-
} else {
|
|
713
|
-
frame.removeAttribute("data-dsh-fe-rail");
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
const releaseRail = () => {
|
|
717
|
-
railActive = false;
|
|
718
|
-
applyRail();
|
|
719
|
-
};
|
|
720
|
-
let railObserver = null;
|
|
721
|
-
if (typeof MutationObserver === "function") {
|
|
722
|
-
railObserver = new MutationObserver(() => {
|
|
723
|
-
if (railActive) syncRailVar();
|
|
724
|
-
});
|
|
725
|
-
ctx.effect(() => {
|
|
726
|
-
const frame = frameOfDetails();
|
|
727
|
-
if (frame !== null) railObserver.observe(frame, { attributes: true, attributeFilter: ["style"] });
|
|
728
|
-
return () => {
|
|
729
|
-
railObserver?.disconnect();
|
|
730
|
-
const frame = frameOfDetails();
|
|
731
|
-
if (frame !== null) frame.removeAttribute("data-dsh-fe-rail");
|
|
732
|
-
railActive = false;
|
|
733
|
-
};
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
// -----------------------------------------------------------------------
|
|
737
|
-
|
|
738
742
|
async function api(method, payload) {
|
|
739
743
|
const options = { headers: { "content-type": "application/json" } };
|
|
740
744
|
options.method = "POST";
|
|
@@ -868,16 +872,16 @@ window.__ModuleLoader__.load({
|
|
|
868
872
|
return STATUS_COLORS[letter] || '#6b7280'
|
|
869
873
|
}
|
|
870
874
|
|
|
871
|
-
function relTime(epochSeconds) {
|
|
875
|
+
function relTime(epochSeconds, t) {
|
|
872
876
|
if (typeof epochSeconds !== 'number' || !Number.isFinite(epochSeconds) || epochSeconds <= 0) return ''
|
|
873
877
|
const s = Math.max(0, Date.now() / 1000 - epochSeconds)
|
|
874
|
-
if (s < 60) return '
|
|
875
|
-
if (s < 3600) return
|
|
876
|
-
if (s < 86400) return
|
|
877
|
-
if (s < 604800) return
|
|
878
|
-
if (s < 2592000) return
|
|
879
|
-
if (s < 31536000) return
|
|
880
|
-
return
|
|
878
|
+
if (s < 60) return t('time.justNow')
|
|
879
|
+
if (s < 3600) return t('time.minutesAgo', { n: Math.floor(s / 60) })
|
|
880
|
+
if (s < 86400) return t('time.hoursAgo', { n: Math.floor(s / 3600) })
|
|
881
|
+
if (s < 604800) return t('time.daysAgo', { n: Math.floor(s / 86400) })
|
|
882
|
+
if (s < 2592000) return t('time.weeksAgo', { n: Math.floor(s / 604800) })
|
|
883
|
+
if (s < 31536000) return t('time.monthsAgo', { n: Math.floor(s / 2592000) })
|
|
884
|
+
return t('time.yearsAgo', { n: Math.floor(s / 31536000) })
|
|
881
885
|
}
|
|
882
886
|
|
|
883
887
|
function shortHash(hash) {
|
|
@@ -1092,7 +1096,8 @@ window.__ModuleLoader__.load({
|
|
|
1092
1096
|
// line, the commit lane rows with an expandable per-commit detail, and a
|
|
1093
1097
|
// diff modal. Data comes entirely from the /_dsh/file-explorer/git-*
|
|
1094
1098
|
// JSON API.
|
|
1095
|
-
function
|
|
1099
|
+
function createGitGraphView(t) {
|
|
1100
|
+
return function GitGraphView(props) {
|
|
1096
1101
|
const api = props.api
|
|
1097
1102
|
const cwd = props.cwd
|
|
1098
1103
|
const [scope, setScope] = React.useState('all')
|
|
@@ -1120,7 +1125,7 @@ window.__ModuleLoader__.load({
|
|
|
1120
1125
|
if (raw && raw.ok === true) {
|
|
1121
1126
|
setLog(raw)
|
|
1122
1127
|
} else {
|
|
1123
|
-
setError(raw && typeof raw.error === 'string' ? raw.error : '
|
|
1128
|
+
setError(raw && typeof raw.error === 'string' ? raw.error : t('git.logFailed'))
|
|
1124
1129
|
}
|
|
1125
1130
|
})
|
|
1126
1131
|
.catch((err) => {
|
|
@@ -1146,7 +1151,7 @@ window.__ModuleLoader__.load({
|
|
|
1146
1151
|
if (raw && raw.ok === true) {
|
|
1147
1152
|
setDetails((prev) => ({ ...prev, [selected]: raw }))
|
|
1148
1153
|
} else {
|
|
1149
|
-
setDetails((prev) => ({ ...prev, [selected]: { error: raw && typeof raw.error === 'string' ? raw.error : '
|
|
1154
|
+
setDetails((prev) => ({ ...prev, [selected]: { error: raw && typeof raw.error === 'string' ? raw.error : t('git.commitFailed') } }))
|
|
1150
1155
|
}
|
|
1151
1156
|
})
|
|
1152
1157
|
.catch((err) => {
|
|
@@ -1165,10 +1170,10 @@ window.__ModuleLoader__.load({
|
|
|
1165
1170
|
api('git-diff', { path: cwd, hash, file: file.path })
|
|
1166
1171
|
.then((raw) => {
|
|
1167
1172
|
if (raw && raw.ok === true) {
|
|
1168
|
-
const note = raw.truncated ? '\n\n
|
|
1173
|
+
const note = raw.truncated ? '\n\n' + t('git.diffTruncated') : ''
|
|
1169
1174
|
setDiff({ title: file.path, hash, loading: false, text: (raw.patch || '') + note, error: null })
|
|
1170
1175
|
} else {
|
|
1171
|
-
setDiff({ title: file.path, hash, loading: false, text: null, error: raw && typeof raw.error === 'string' ? raw.error : '
|
|
1176
|
+
setDiff({ title: file.path, hash, loading: false, text: null, error: raw && typeof raw.error === 'string' ? raw.error : t('git.diffFailed') })
|
|
1172
1177
|
}
|
|
1173
1178
|
})
|
|
1174
1179
|
.catch((err) => {
|
|
@@ -1182,7 +1187,7 @@ window.__ModuleLoader__.load({
|
|
|
1182
1187
|
const renderCommitDetail = (hash, d) => {
|
|
1183
1188
|
if (d === undefined) {
|
|
1184
1189
|
return React.createElement('div', { className: 'dsh-git-detail' },
|
|
1185
|
-
React.createElement('div', { className: 'dsh-git-files-title' }, '
|
|
1190
|
+
React.createElement('div', { className: 'dsh-git-files-title' }, t('git.loadingChanges')))
|
|
1186
1191
|
}
|
|
1187
1192
|
if (d.error) {
|
|
1188
1193
|
return React.createElement('div', { className: 'dsh-git-detail' },
|
|
@@ -1190,9 +1195,9 @@ window.__ModuleLoader__.load({
|
|
|
1190
1195
|
}
|
|
1191
1196
|
const files = Array.isArray(d.files) ? d.files : []
|
|
1192
1197
|
return React.createElement('div', { className: 'dsh-git-detail' },
|
|
1193
|
-
React.createElement('div', { className: 'dsh-git-msg' }, d.message ||
|
|
1198
|
+
React.createElement('div', { className: 'dsh-git-msg' }, d.message || t('git.noMessage')),
|
|
1194
1199
|
React.createElement('div', { className: 'dsh-git-files-title' },
|
|
1195
|
-
|
|
1200
|
+
files.length === 1 ? t('git.changedFiles.one', { count: files.length }) : t('git.changedFiles.other', { count: files.length })),
|
|
1196
1201
|
files.length === 0 ? null : files.map((f, i) =>
|
|
1197
1202
|
React.createElement(GitFileRow, { key: i, file: f, onOpen: (file) => openDiff(hash, file) })),
|
|
1198
1203
|
)
|
|
@@ -1200,7 +1205,7 @@ window.__ModuleLoader__.load({
|
|
|
1200
1205
|
|
|
1201
1206
|
const rows = []
|
|
1202
1207
|
if (loading) {
|
|
1203
|
-
rows.push(React.createElement('div', { key: 'loading', className: 'dsh-fe-status' }, '
|
|
1208
|
+
rows.push(React.createElement('div', { key: 'loading', className: 'dsh-fe-status' }, t('git.loading')))
|
|
1204
1209
|
} else if (error) {
|
|
1205
1210
|
rows.push(React.createElement('div', { key: 'error', className: 'dsh-fe-status dsh-fe-status-error' }, error))
|
|
1206
1211
|
} else if (graph !== null) {
|
|
@@ -1214,21 +1219,21 @@ window.__ModuleLoader__.load({
|
|
|
1214
1219
|
className: 'dsh-git-row',
|
|
1215
1220
|
onClick: () => toggle('WORKING'),
|
|
1216
1221
|
role: 'button',
|
|
1217
|
-
title: '
|
|
1222
|
+
title: t('git.uncommitted'),
|
|
1218
1223
|
},
|
|
1219
1224
|
React.createElement(WorkTreeCell, { lane: first !== null ? first.lane : 0, connectorColor }),
|
|
1220
1225
|
React.createElement('div', { className: 'dsh-git-main' },
|
|
1221
1226
|
React.createElement('div', { className: 'dsh-git-line1' },
|
|
1222
|
-
React.createElement('span', { className: 'dsh-git-subject' }, '
|
|
1227
|
+
React.createElement('span', { className: 'dsh-git-subject' }, t('git.uncommitted')),
|
|
1223
1228
|
React.createElement('span', { className: 'dsh-git-pill dsh-git-pill-branch' }, String(changes.length)),
|
|
1224
1229
|
),
|
|
1225
|
-
React.createElement('div', { className: 'dsh-git-line2' }, '
|
|
1230
|
+
React.createElement('div', { className: 'dsh-git-line2' }, t('git.workingTree')),
|
|
1226
1231
|
),
|
|
1227
1232
|
),
|
|
1228
1233
|
selected === 'WORKING'
|
|
1229
1234
|
? React.createElement('div', { className: 'dsh-git-detail' },
|
|
1230
1235
|
React.createElement('div', { className: 'dsh-git-files-title' },
|
|
1231
|
-
|
|
1236
|
+
changes.length === 1 ? t('git.changedVsHead.one', { count: changes.length }) : t('git.changedVsHead.other', { count: changes.length })),
|
|
1232
1237
|
changes.map((f, i) =>
|
|
1233
1238
|
React.createElement(GitFileRow, { key: i, file: f, onOpen: (file) => openDiff('WORKING', file) })),
|
|
1234
1239
|
)
|
|
@@ -1251,23 +1256,23 @@ window.__ModuleLoader__.load({
|
|
|
1251
1256
|
React.createElement('span', { className: 'dsh-git-subject' }, c.subject),
|
|
1252
1257
|
),
|
|
1253
1258
|
React.createElement('div', { className: 'dsh-git-line2' },
|
|
1254
|
-
`${c.author || '
|
|
1259
|
+
`${c.author || t('git.authorUnknown')} · ${relTime(c.date, t)} · ${shortHash(c.hash)}`),
|
|
1255
1260
|
),
|
|
1256
1261
|
),
|
|
1257
1262
|
selected === c.hash ? renderCommitDetail(c.hash, details[c.hash]) : null,
|
|
1258
1263
|
))
|
|
1259
1264
|
})
|
|
1260
1265
|
if (rows.length === 0) {
|
|
1261
|
-
rows.push(React.createElement('div', { key: 'empty', className: 'dsh-fe-status' }, '
|
|
1266
|
+
rows.push(React.createElement('div', { key: 'empty', className: 'dsh-fe-status' }, t('git.noCommitsYet')))
|
|
1262
1267
|
}
|
|
1263
1268
|
} else {
|
|
1264
|
-
rows.push(React.createElement('div', { key: 'none', className: 'dsh-fe-status' }, '
|
|
1269
|
+
rows.push(React.createElement('div', { key: 'none', className: 'dsh-fe-status' }, t('git.noData')))
|
|
1265
1270
|
}
|
|
1266
1271
|
|
|
1267
1272
|
const branchLabel = status
|
|
1268
1273
|
? (status.branch === null
|
|
1269
|
-
? '
|
|
1270
|
-
: status.branch + (status.unborn ? '
|
|
1274
|
+
? t('git.detachedHead')
|
|
1275
|
+
: status.branch + (status.unborn ? t('git.noCommitsYetSuffix') : ''))
|
|
1271
1276
|
: null
|
|
1272
1277
|
const upstreamBits = []
|
|
1273
1278
|
if (status && status.ahead > 0) upstreamBits.push('↑' + status.ahead)
|
|
@@ -1279,17 +1284,17 @@ window.__ModuleLoader__.load({
|
|
|
1279
1284
|
type: 'button',
|
|
1280
1285
|
className: 'dsh-fe-toolbutton',
|
|
1281
1286
|
onClick: reload,
|
|
1282
|
-
'aria-label': '
|
|
1283
|
-
title: '
|
|
1287
|
+
'aria-label': t('git.refreshGraph'),
|
|
1288
|
+
title: t('files.refresh'),
|
|
1284
1289
|
}, svgIcon(refreshIcon, 15)),
|
|
1285
1290
|
React.createElement('select', {
|
|
1286
1291
|
className: 'dsh-git-scope',
|
|
1287
1292
|
value: scope,
|
|
1288
|
-
'aria-label': '
|
|
1293
|
+
'aria-label': t('git.branchFilter'),
|
|
1289
1294
|
onChange: (event) => setScope(event.target.value),
|
|
1290
1295
|
},
|
|
1291
|
-
React.createElement('option', { value: 'all' }, '
|
|
1292
|
-
React.createElement('option', { value: 'current' }, '
|
|
1296
|
+
React.createElement('option', { value: 'all' }, t('git.allBranches')),
|
|
1297
|
+
React.createElement('option', { value: 'current' }, t('git.currentBranch')),
|
|
1293
1298
|
),
|
|
1294
1299
|
),
|
|
1295
1300
|
branchLabel !== null
|
|
@@ -1297,7 +1302,7 @@ window.__ModuleLoader__.load({
|
|
|
1297
1302
|
svgIcon(branchIcon, 12),
|
|
1298
1303
|
React.createElement('span', { className: 'dsh-git-info-name' }, branchLabel),
|
|
1299
1304
|
upstreamBits.length > 0 ? React.createElement('span', null, upstreamBits.join(' ')) : null,
|
|
1300
|
-
React.createElement('span', null,
|
|
1305
|
+
React.createElement('span', null, changes.length === 1 ? t('git.changesCount.one', { count: changes.length }) : t('git.changesCount.other', { count: changes.length })),
|
|
1301
1306
|
)
|
|
1302
1307
|
: null,
|
|
1303
1308
|
React.createElement('div', { className: 'dsh-git-list' }, rows),
|
|
@@ -1309,29 +1314,31 @@ window.__ModuleLoader__.load({
|
|
|
1309
1314
|
React.createElement('div', {
|
|
1310
1315
|
className: 'dsh-fe-modal',
|
|
1311
1316
|
role: 'dialog',
|
|
1312
|
-
'aria-label': '
|
|
1317
|
+
'aria-label': t('git.diff'),
|
|
1313
1318
|
onClick: (event) => event.stopPropagation(),
|
|
1314
1319
|
},
|
|
1315
1320
|
React.createElement('div', { className: 'dsh-fe-modal-head' },
|
|
1316
1321
|
React.createElement('div', { className: 'dsh-fe-modal-title' }, diff.title),
|
|
1317
1322
|
React.createElement('div', { className: 'dsh-fe-modal-meta' },
|
|
1318
|
-
diff.hash === 'WORKING' ? '
|
|
1323
|
+
diff.hash === 'WORKING' ? t('git.workingTreeLabel') : shortHash(diff.hash)),
|
|
1319
1324
|
React.createElement('button', {
|
|
1320
1325
|
type: 'button',
|
|
1321
1326
|
className: 'dsh-fe-icon',
|
|
1322
1327
|
onClick: () => setDiff(null),
|
|
1323
|
-
'aria-label': '
|
|
1328
|
+
'aria-label': t('git.closeDiff'),
|
|
1324
1329
|
}, svgIcon(closeIcon, 14)),
|
|
1325
1330
|
),
|
|
1326
1331
|
React.createElement('div', { className: 'dsh-fe-modal-body' },
|
|
1327
|
-
diff.loading ? '
|
|
1332
|
+
diff.loading ? t('git.loading') : (diff.error || diff.text || t('git.noDiff'))),
|
|
1328
1333
|
),
|
|
1329
1334
|
)
|
|
1330
1335
|
: null,
|
|
1331
1336
|
)
|
|
1332
1337
|
}
|
|
1338
|
+
}
|
|
1333
1339
|
|
|
1334
|
-
function
|
|
1340
|
+
function createFileExplorerPage(t) {
|
|
1341
|
+
return function FileExplorerPage(props) {
|
|
1335
1342
|
if (typeof props.useSessions !== 'function') return null
|
|
1336
1343
|
|
|
1337
1344
|
const sessionId = props.useSessions((state) => {
|
|
@@ -1364,23 +1371,11 @@ window.__ModuleLoader__.load({
|
|
|
1364
1371
|
const [imageDragging, setImageDragging] = React.useState(false)
|
|
1365
1372
|
const imageDrag = React.useRef(null)
|
|
1366
1373
|
const [pendingDelete, setPendingDelete] = React.useState(null)
|
|
1367
|
-
const [view, setView] = React.useState('files')
|
|
1368
|
-
const [collapsed, setCollapsed] = React.useState(false)
|
|
1369
|
-
|
|
1370
|
-
React.useEffect(() => {
|
|
1371
|
-
if (sessionId === undefined) return
|
|
1372
|
-
setCollapsed(false)
|
|
1373
|
-
if (layout !== undefined) {
|
|
1374
|
-
try { layout.openDetails() } catch (_e) { }
|
|
1375
|
-
}
|
|
1376
|
-
// Same-tick store updates batch in React, so restoring right after
|
|
1377
|
-
// openDetails() replaces the 360px default without a visible jump.
|
|
1378
|
-
restoreWidth()
|
|
1379
|
-
}, [sessionId])
|
|
1380
1374
|
|
|
1381
1375
|
React.useEffect(() => {
|
|
1382
1376
|
if (sessionId === undefined) {
|
|
1383
|
-
|
|
1377
|
+
setParentPath(null)
|
|
1378
|
+
setRequestPath('')
|
|
1384
1379
|
return
|
|
1385
1380
|
}
|
|
1386
1381
|
setParentPath(null)
|
|
@@ -1408,7 +1403,7 @@ window.__ModuleLoader__.load({
|
|
|
1408
1403
|
setCurrentPath(requestPath)
|
|
1409
1404
|
setParentPath(null)
|
|
1410
1405
|
setEntries([])
|
|
1411
|
-
setError(raw && typeof raw.error === 'string' ? raw.error : '
|
|
1406
|
+
setError(raw && typeof raw.error === 'string' ? raw.error : t('files.listFailed'))
|
|
1412
1407
|
}
|
|
1413
1408
|
})
|
|
1414
1409
|
.catch((err) => {
|
|
@@ -1432,34 +1427,6 @@ window.__ModuleLoader__.load({
|
|
|
1432
1427
|
return aDir - bDir || a.name.localeCompare(b.name)
|
|
1433
1428
|
})
|
|
1434
1429
|
const isRoot = parentPath === null || parentPath === currentPath
|
|
1435
|
-
// Collapse into a slim icon rail that keeps a real 56px column in
|
|
1436
|
-
// the layout (like the left sidebar's rail): the details store stays
|
|
1437
|
-
// closed (no drag handle / border), and applyRail() toggles the
|
|
1438
|
-
// [data-dsh-fe-rail] attribute so the !important stylesheet rule
|
|
1439
|
-
// overrides the frame's third grid track. A MutationObserver keeps
|
|
1440
|
-
// the sidebar-width variable fresh across app re-renders.
|
|
1441
|
-
const collapse = () => {
|
|
1442
|
-
setCollapsed(true)
|
|
1443
|
-
railActive = true
|
|
1444
|
-
closePreview()
|
|
1445
|
-
setPendingDelete(null)
|
|
1446
|
-
setEditingPath(false)
|
|
1447
|
-
if (layout !== undefined) {
|
|
1448
|
-
try { layout.closeDetails() } catch (_e) { }
|
|
1449
|
-
}
|
|
1450
|
-
applyRail()
|
|
1451
|
-
}
|
|
1452
|
-
const expand = (nextView) => {
|
|
1453
|
-
releaseRail()
|
|
1454
|
-
setCollapsed(false)
|
|
1455
|
-
setView(nextView)
|
|
1456
|
-
if (layout !== undefined) {
|
|
1457
|
-
try { layout.openDetails() } catch (_e) { }
|
|
1458
|
-
}
|
|
1459
|
-
// Same-tick store updates batch in React, so restoring right after
|
|
1460
|
-
// openDetails() replaces the 360px default without a visible jump.
|
|
1461
|
-
restoreWidth()
|
|
1462
|
-
}
|
|
1463
1430
|
const reload = () => { setPendingDelete(null); setReloadTick((tick) => tick + 1) }
|
|
1464
1431
|
const startEditPath = () => {
|
|
1465
1432
|
setDraftPath(currentPath)
|
|
@@ -1538,7 +1505,7 @@ window.__ModuleLoader__.load({
|
|
|
1538
1505
|
setPreview(null)
|
|
1539
1506
|
api('read', { path: entry.path })
|
|
1540
1507
|
.then((raw) => {
|
|
1541
|
-
setPreview(raw && raw.ok === true ? raw : { ok: false, error: raw && typeof raw.error === 'string' ? raw.error : '
|
|
1508
|
+
setPreview(raw && raw.ok === true ? raw : { ok: false, error: raw && typeof raw.error === 'string' ? raw.error : t('files.readFailed') })
|
|
1542
1509
|
})
|
|
1543
1510
|
.catch((err) => {
|
|
1544
1511
|
setPreview({ ok: false, error: err && typeof err.message === 'string' ? err.message : String(err) })
|
|
@@ -1551,7 +1518,7 @@ window.__ModuleLoader__.load({
|
|
|
1551
1518
|
api('download', { path: entry.path })
|
|
1552
1519
|
.then((raw) => {
|
|
1553
1520
|
if (!raw || raw.ok !== true) {
|
|
1554
|
-
const message = raw && typeof raw.error === 'string' ? raw.error : '
|
|
1521
|
+
const message = raw && typeof raw.error === 'string' ? raw.error : t('files.downloadFailed')
|
|
1555
1522
|
setError(message)
|
|
1556
1523
|
return
|
|
1557
1524
|
}
|
|
@@ -1578,7 +1545,7 @@ window.__ModuleLoader__.load({
|
|
|
1578
1545
|
api('delete', { path: entry.path })
|
|
1579
1546
|
.then((raw) => {
|
|
1580
1547
|
if (!raw || raw.ok !== true) {
|
|
1581
|
-
setError(raw && typeof raw.error === 'string' ? raw.error : '
|
|
1548
|
+
setError(raw && typeof raw.error === 'string' ? raw.error : t('files.deleteFailed'))
|
|
1582
1549
|
return
|
|
1583
1550
|
}
|
|
1584
1551
|
reload()
|
|
@@ -1602,9 +1569,9 @@ window.__ModuleLoader__.load({
|
|
|
1602
1569
|
: React.createElement('button', {
|
|
1603
1570
|
type: 'button',
|
|
1604
1571
|
className: 'dsh-fe-path',
|
|
1605
|
-
title: currentPath || '
|
|
1572
|
+
title: currentPath || t('files.workspaceRoot'),
|
|
1606
1573
|
onClick: startEditPath,
|
|
1607
|
-
}, currentPath || '
|
|
1574
|
+
}, currentPath || t('files.workspaceRoot'))
|
|
1608
1575
|
|
|
1609
1576
|
const filesView = React.createElement(React.Fragment, null,
|
|
1610
1577
|
pathControl,
|
|
@@ -1616,24 +1583,24 @@ window.__ModuleLoader__.load({
|
|
|
1616
1583
|
onClick: () => {
|
|
1617
1584
|
if (parentPath !== null && parentPath !== currentPath) setRequestPath(parentPath)
|
|
1618
1585
|
},
|
|
1619
|
-
'aria-label': '
|
|
1620
|
-
title: '
|
|
1586
|
+
'aria-label': t('files.goUp'),
|
|
1587
|
+
title: t('files.up'),
|
|
1621
1588
|
}, svgIcon(upIcon, 15)),
|
|
1622
1589
|
React.createElement('button', {
|
|
1623
1590
|
type: 'button',
|
|
1624
1591
|
className: 'dsh-fe-toolbutton',
|
|
1625
1592
|
onClick: reload,
|
|
1626
|
-
'aria-label': '
|
|
1627
|
-
title: '
|
|
1593
|
+
'aria-label': t('files.refreshList'),
|
|
1594
|
+
title: t('files.refresh'),
|
|
1628
1595
|
}, svgIcon(refreshIcon, 15)),
|
|
1629
1596
|
),
|
|
1630
1597
|
React.createElement('div', { className: 'dsh-fe-list' },
|
|
1631
1598
|
loading
|
|
1632
|
-
? React.createElement('div', { className: 'dsh-fe-status' }, '
|
|
1599
|
+
? React.createElement('div', { className: 'dsh-fe-status' }, t('files.loading'))
|
|
1633
1600
|
: error
|
|
1634
1601
|
? React.createElement('div', { className: 'dsh-fe-status dsh-fe-status-error' }, error)
|
|
1635
1602
|
: sorted.length === 0
|
|
1636
|
-
? React.createElement('div', { className: 'dsh-fe-status' }, '
|
|
1603
|
+
? React.createElement('div', { className: 'dsh-fe-status' }, t('files.empty'))
|
|
1637
1604
|
: sorted.map((entry) => {
|
|
1638
1605
|
const isDir = entry.type === 'directory'
|
|
1639
1606
|
return React.createElement('div', {
|
|
@@ -1654,22 +1621,22 @@ window.__ModuleLoader__.load({
|
|
|
1654
1621
|
type: 'button',
|
|
1655
1622
|
className: 'dsh-fe-action',
|
|
1656
1623
|
onClick: (event) => { event.stopPropagation(); openPreview(entry) },
|
|
1657
|
-
'aria-label':
|
|
1658
|
-
title: '
|
|
1624
|
+
'aria-label': t('files.viewFile', { name: entry.name }),
|
|
1625
|
+
title: t('files.view'),
|
|
1659
1626
|
}, svgIcon(eyeIcon, 14)),
|
|
1660
1627
|
!isDir && React.createElement('button', {
|
|
1661
1628
|
type: 'button',
|
|
1662
1629
|
className: 'dsh-fe-action',
|
|
1663
1630
|
onClick: (event) => { event.stopPropagation(); downloadFile(entry) },
|
|
1664
|
-
'aria-label':
|
|
1665
|
-
title: '
|
|
1631
|
+
'aria-label': t('files.downloadFile', { name: entry.name }),
|
|
1632
|
+
title: t('files.download'),
|
|
1666
1633
|
}, svgIcon(downloadIcon, 14)),
|
|
1667
1634
|
React.createElement('button', {
|
|
1668
1635
|
type: 'button',
|
|
1669
1636
|
className: pendingDelete === entry.path ? 'dsh-fe-action dsh-fe-action-confirm' : 'dsh-fe-action dsh-fe-action-danger',
|
|
1670
1637
|
onClick: (event) => { event.stopPropagation(); requestDelete(entry) },
|
|
1671
|
-
'aria-label': pendingDelete === entry.path ?
|
|
1672
|
-
title: pendingDelete === entry.path ? '
|
|
1638
|
+
'aria-label': pendingDelete === entry.path ? t('files.confirmDelete', { name: entry.name }) : t('files.deleteFile', { name: entry.name }),
|
|
1639
|
+
title: pendingDelete === entry.path ? t('files.clickAgain') : t('files.delete'),
|
|
1673
1640
|
}, svgIcon(trashIcon, 14)),
|
|
1674
1641
|
),
|
|
1675
1642
|
),
|
|
@@ -1678,65 +1645,13 @@ window.__ModuleLoader__.load({
|
|
|
1678
1645
|
),
|
|
1679
1646
|
)
|
|
1680
1647
|
|
|
1681
|
-
// Collapsed: a slim icon rail (file / git) that keeps a real 56px
|
|
1682
|
-
// column in the layout (the [data-dsh-fe-rail] !important grid rule;
|
|
1683
|
-
// see applyRail above), mirroring the left sidebar's collapsed rail.
|
|
1684
|
-
// Placed after every handler declaration: a collapsed render must not
|
|
1685
|
-
// return early and leave `expand` uninitialized (TDZ error on click).
|
|
1686
|
-
if (collapsed) {
|
|
1687
|
-
return React.createElement('div', {
|
|
1688
|
-
className: 'dsh-fe-rail',
|
|
1689
|
-
role: 'region',
|
|
1690
|
-
'aria-label': 'File explorer (collapsed)',
|
|
1691
|
-
},
|
|
1692
|
-
React.createElement('button', {
|
|
1693
|
-
type: 'button',
|
|
1694
|
-
className: 'dsh-fe-rail-button',
|
|
1695
|
-
onClick: () => expand('files'),
|
|
1696
|
-
title: 'Files',
|
|
1697
|
-
'aria-label': 'Open file explorer',
|
|
1698
|
-
}, svgIcon(folderIcon, 16)),
|
|
1699
|
-
React.createElement('button', {
|
|
1700
|
-
type: 'button',
|
|
1701
|
-
className: 'dsh-fe-rail-button',
|
|
1702
|
-
onClick: () => expand('git'),
|
|
1703
|
-
title: 'Git Graph',
|
|
1704
|
-
'aria-label': 'Open git graph',
|
|
1705
|
-
}, svgIcon(branchIcon, 16)),
|
|
1706
|
-
)
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
1648
|
return React.createElement(React.Fragment, null,
|
|
1710
1649
|
React.createElement('div', {
|
|
1711
|
-
className: 'dsh-fe-
|
|
1650
|
+
className: 'dsh-fe-page',
|
|
1712
1651
|
role: 'region',
|
|
1713
|
-
'aria-label': '
|
|
1652
|
+
'aria-label': t('files.title'),
|
|
1714
1653
|
},
|
|
1715
|
-
|
|
1716
|
-
React.createElement('button', {
|
|
1717
|
-
type: 'button',
|
|
1718
|
-
className: 'dsh-fe-icon',
|
|
1719
|
-
onClick: collapse,
|
|
1720
|
-
'aria-label': 'Collapse file explorer into icon bar',
|
|
1721
|
-
title: 'Collapse',
|
|
1722
|
-
}, collapseIcon),
|
|
1723
|
-
React.createElement('div', { className: 'dsh-fe-title' }, view === 'git' ? 'Git Graph' : 'Files'),
|
|
1724
|
-
),
|
|
1725
|
-
React.createElement('div', { className: 'dsh-fe-tabs' },
|
|
1726
|
-
React.createElement('button', {
|
|
1727
|
-
type: 'button',
|
|
1728
|
-
className: 'dsh-fe-tab' + (view === 'files' ? ' dsh-fe-tab-active' : ''),
|
|
1729
|
-
onClick: () => setView('files'),
|
|
1730
|
-
}, svgIcon(folderIcon, 13), 'Files'),
|
|
1731
|
-
React.createElement('button', {
|
|
1732
|
-
type: 'button',
|
|
1733
|
-
className: 'dsh-fe-tab' + (view === 'git' ? ' dsh-fe-tab-active' : ''),
|
|
1734
|
-
onClick: () => setView('git'),
|
|
1735
|
-
}, svgIcon(branchIcon, 13), 'Git Graph'),
|
|
1736
|
-
),
|
|
1737
|
-
view === 'git'
|
|
1738
|
-
? React.createElement(GitGraphView, { api, cwd })
|
|
1739
|
-
: filesView,
|
|
1654
|
+
filesView,
|
|
1740
1655
|
),
|
|
1741
1656
|
preview &&
|
|
1742
1657
|
React.createElement('div', {
|
|
@@ -1746,50 +1661,50 @@ window.__ModuleLoader__.load({
|
|
|
1746
1661
|
React.createElement('div', {
|
|
1747
1662
|
className: preview.kind === 'image' && imageFullscreen ? 'dsh-fe-modal dsh-fe-modal-fullscreen' : 'dsh-fe-modal',
|
|
1748
1663
|
role: 'dialog',
|
|
1749
|
-
'aria-label': '
|
|
1664
|
+
'aria-label': t('files.previewDialog'),
|
|
1750
1665
|
'aria-modal': true,
|
|
1751
1666
|
tabIndex: -1,
|
|
1752
1667
|
onKeyDown: handlePreviewKeyDown,
|
|
1753
1668
|
onClick: (event) => event.stopPropagation(),
|
|
1754
1669
|
},
|
|
1755
1670
|
React.createElement('div', { className: 'dsh-fe-modal-head' },
|
|
1756
|
-
React.createElement('div', { className: 'dsh-fe-modal-title' }, preview.name || '
|
|
1757
|
-
React.createElement('div', { className: 'dsh-fe-modal-meta' }, preview.kind === 'image' ?
|
|
1671
|
+
React.createElement('div', { className: 'dsh-fe-modal-title' }, preview.name || t('files.preview')),
|
|
1672
|
+
React.createElement('div', { className: 'dsh-fe-modal-meta' }, preview.kind === 'image' ? t('files.imageMeta', { size: formatSize(preview.size) }) : preview.binary ? t('files.binary') : formatSize(preview.size)),
|
|
1758
1673
|
preview.kind === 'image' && preview.dataUrl
|
|
1759
1674
|
? React.createElement('div', {
|
|
1760
1675
|
className: 'dsh-fe-image-tools',
|
|
1761
1676
|
role: 'toolbar',
|
|
1762
|
-
'aria-label': '
|
|
1677
|
+
'aria-label': t('files.imageControls'),
|
|
1763
1678
|
},
|
|
1764
1679
|
React.createElement('button', {
|
|
1765
1680
|
type: 'button',
|
|
1766
1681
|
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1767
1682
|
disabled: imageScale <= 0.25,
|
|
1768
1683
|
onClick: () => changeImageZoom(-0.25),
|
|
1769
|
-
'aria-label': '
|
|
1770
|
-
title: '
|
|
1684
|
+
'aria-label': t('files.zoomOut'),
|
|
1685
|
+
title: t('files.zoomOut'),
|
|
1771
1686
|
}, svgIcon(zoomOutIcon, 14)),
|
|
1772
1687
|
React.createElement('button', {
|
|
1773
1688
|
type: 'button',
|
|
1774
1689
|
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1775
1690
|
onClick: resetImageZoom,
|
|
1776
|
-
'aria-label': '
|
|
1777
|
-
title: '
|
|
1691
|
+
'aria-label': t('files.resetImageZoom'),
|
|
1692
|
+
title: t('files.resetZoom'),
|
|
1778
1693
|
}, React.createElement('span', { className: 'dsh-fe-image-zoom-label' }, `${Math.round(imageScale * 100)}%`)),
|
|
1779
1694
|
React.createElement('button', {
|
|
1780
1695
|
type: 'button',
|
|
1781
1696
|
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1782
1697
|
disabled: imageScale >= 4,
|
|
1783
1698
|
onClick: () => changeImageZoom(0.25),
|
|
1784
|
-
'aria-label': '
|
|
1785
|
-
title: '
|
|
1699
|
+
'aria-label': t('files.zoomIn'),
|
|
1700
|
+
title: t('files.zoomIn'),
|
|
1786
1701
|
}, svgIcon(zoomInIcon, 14)),
|
|
1787
1702
|
React.createElement('button', {
|
|
1788
1703
|
type: 'button',
|
|
1789
1704
|
className: 'dsh-fe-icon dsh-fe-image-tool',
|
|
1790
1705
|
onClick: () => setImageFullscreen((value) => !value),
|
|
1791
|
-
'aria-label': imageFullscreen ? '
|
|
1792
|
-
title: imageFullscreen ? '
|
|
1706
|
+
'aria-label': imageFullscreen ? t('files.exitFullscreen') : t('files.enterFullscreen'),
|
|
1707
|
+
title: imageFullscreen ? t('files.exitFullscreen') : t('files.fullscreen'),
|
|
1793
1708
|
}, svgIcon(imageFullscreen ? fullscreenExitIcon : fullscreenIcon, 14)),
|
|
1794
1709
|
)
|
|
1795
1710
|
: null,
|
|
@@ -1797,7 +1712,7 @@ window.__ModuleLoader__.load({
|
|
|
1797
1712
|
type: 'button',
|
|
1798
1713
|
className: 'dsh-fe-icon',
|
|
1799
1714
|
onClick: closePreview,
|
|
1800
|
-
'aria-label': '
|
|
1715
|
+
'aria-label': t('files.closePreview'),
|
|
1801
1716
|
}, svgIcon(closeIcon, 14)),
|
|
1802
1717
|
),
|
|
1803
1718
|
React.createElement('div', {
|
|
@@ -1809,7 +1724,7 @@ window.__ModuleLoader__.load({
|
|
|
1809
1724
|
onPointerCancel: preview.kind === 'image' && preview.dataUrl ? endImageDrag : undefined,
|
|
1810
1725
|
},
|
|
1811
1726
|
previewLoading
|
|
1812
|
-
? '
|
|
1727
|
+
? t('files.loading')
|
|
1813
1728
|
: preview.error
|
|
1814
1729
|
? preview.error
|
|
1815
1730
|
: preview.kind === 'image'
|
|
@@ -1819,26 +1734,86 @@ window.__ModuleLoader__.load({
|
|
|
1819
1734
|
className: 'dsh-fe-preview-image' + (imageScale > 1 ? ' dsh-fe-preview-image-pannable' : '') + (imageDragging ? ' dsh-fe-preview-image-dragging' : ''),
|
|
1820
1735
|
style: { transform: `translate3d(${imageOffset.x}px, ${imageOffset.y}px, 0) scale(${imageScale})` },
|
|
1821
1736
|
src: preview.dataUrl,
|
|
1822
|
-
alt: preview.name || '
|
|
1737
|
+
alt: preview.name || t('files.imagePreview'),
|
|
1823
1738
|
draggable: false,
|
|
1824
1739
|
})
|
|
1825
1740
|
)
|
|
1826
|
-
: preview.tooLarge ? '
|
|
1741
|
+
: preview.tooLarge ? t('files.imageTooLarge') : t('files.noPreview')
|
|
1827
1742
|
: preview.text !== undefined && preview.text !== null
|
|
1828
1743
|
? preview.text
|
|
1829
1744
|
: preview.tooLarge
|
|
1830
|
-
? '
|
|
1831
|
-
: preview.binary ? '
|
|
1745
|
+
? t('files.fileTooLarge')
|
|
1746
|
+
: preview.binary ? t('files.binaryNoPreview') : t('files.noPreview'),
|
|
1832
1747
|
),
|
|
1833
1748
|
),
|
|
1834
1749
|
),
|
|
1835
1750
|
)
|
|
1836
1751
|
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function createFileExplorerGitPage(t) {
|
|
1755
|
+
return function FileExplorerGitPage(props) {
|
|
1756
|
+
if (typeof props.useSessions !== 'function') return null
|
|
1757
|
+
|
|
1758
|
+
const sessionId = props.useSessions((state) => {
|
|
1759
|
+
const current = state.current
|
|
1760
|
+
if (current === undefined) return undefined
|
|
1761
|
+
const row = state.byId[current]
|
|
1762
|
+
if (!row || row.blank === true) return undefined
|
|
1763
|
+
return current
|
|
1764
|
+
})
|
|
1765
|
+
const cwd = props.useSessions((state) => {
|
|
1766
|
+
const current = state.current
|
|
1767
|
+
if (current === undefined) return undefined
|
|
1768
|
+
const row = state.byId[current]
|
|
1769
|
+
return row ? row.cwd : undefined
|
|
1770
|
+
})
|
|
1771
|
+
|
|
1772
|
+
if (sessionId === undefined) return null
|
|
1773
|
+
return React.createElement('div', {
|
|
1774
|
+
className: 'dsh-fe-page',
|
|
1775
|
+
role: 'region',
|
|
1776
|
+
'aria-label': t('git.title'),
|
|
1777
|
+
}, React.createElement(GitGraphView, { api, cwd }))
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const GitGraphView = createGitGraphView(t);
|
|
1782
|
+
const FileExplorerPage = createFileExplorerPage(t);
|
|
1783
|
+
const FileExplorerGitPage = createFileExplorerGitPage(t);
|
|
1837
1784
|
|
|
1838
|
-
ctx.effect(() =>
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1785
|
+
ctx.effect(() => {
|
|
1786
|
+
const disposeFilesPage = rightPanel.registerPage({
|
|
1787
|
+
id: 'file-explorer.files',
|
|
1788
|
+
title: t('files.title'),
|
|
1789
|
+
group: t('workspace'),
|
|
1790
|
+
order: 20,
|
|
1791
|
+
placement: 'rail',
|
|
1792
|
+
icon: (size) => svgIcon(folderIcon, size),
|
|
1793
|
+
})
|
|
1794
|
+
const disposeGitPage = rightPanel.registerPage({
|
|
1795
|
+
id: 'file-explorer.git',
|
|
1796
|
+
title: t('git.title'),
|
|
1797
|
+
group: t('workspace'),
|
|
1798
|
+
order: 21,
|
|
1799
|
+
placement: 'rail',
|
|
1800
|
+
icon: (size) => svgIcon(branchIcon, size),
|
|
1801
|
+
})
|
|
1802
|
+
const disposeFilesSlot = slots.inject('right-panel.page', () => slots.register({
|
|
1803
|
+
name: 'right-panel.page',
|
|
1804
|
+
key: 'file-explorer.files',
|
|
1805
|
+
}, FileExplorerPage))
|
|
1806
|
+
const disposeGitSlot = slots.inject('right-panel.page', () => slots.register({
|
|
1807
|
+
name: 'right-panel.page',
|
|
1808
|
+
key: 'file-explorer.git',
|
|
1809
|
+
}, FileExplorerGitPage))
|
|
1810
|
+
return () => {
|
|
1811
|
+
disposeFilesSlot()
|
|
1812
|
+
disposeGitSlot()
|
|
1813
|
+
disposeFilesPage()
|
|
1814
|
+
disposeGitPage()
|
|
1815
|
+
}
|
|
1816
|
+
})
|
|
1842
1817
|
}
|
|
1843
1818
|
|
|
1844
1819
|
exports.apply = apply;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yiln-dsh/dsh-plugin-file-explorer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"description": "DSH bundle plugin that
|
|
7
|
+
"description": "DSH bundle plugin that contributes a workspace file explorer page to the right-panel host.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"exports": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"client": {
|
|
20
20
|
"inject": [
|
|
21
|
+
"@yiln-dsh/dsh-plugin-right-panel",
|
|
21
22
|
"@deepseek-ai/dsh-client-runtime",
|
|
22
23
|
"@deepseek-ai/dsh-client-ui-layout",
|
|
23
24
|
"@deepseek-ai/dsh-client-ui-renderer"
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
36
37
|
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.0",
|
|
37
38
|
"@deepseek-ai/dsh-client-ui-layout": ">=0.1.0-rc.0",
|
|
38
|
-
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.0-rc.0"
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.0-rc.0",
|
|
40
|
+
"@yiln-dsh/dsh-plugin-right-panel": ">=0.1.0"
|
|
39
41
|
},
|
|
40
42
|
"engines": {
|
|
41
43
|
"node": ">=22"
|