@restregular/dsh-filekit 0.0.0-stage → 0.2.2
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/LICENSE +21 -0
- package/README.md +160 -2
- package/client.js +3385 -0
- package/cordis.patch.yml +10 -0
- package/icon.svg +15 -0
- package/index.js +651 -0
- package/lib/filekit.js +586 -0
- package/lib/notice.js +48 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -4
package/client.js
ADDED
|
@@ -0,0 +1,3385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-filekit — Client half.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* 1. an extension over the shipped `files` tab kind: a workspace tree with
|
|
6
|
+
* right-click actions and drag-to-move;
|
|
7
|
+
* 2. a FileKit page tab (`filekit` kind) holding the full manager: browse,
|
|
8
|
+
* text editor, recycle bin;
|
|
9
|
+
* 3. a custom guide card for the workspace-files entry with a second button
|
|
10
|
+
* that opens the FileKit page in the sidebar;
|
|
11
|
+
* 4. header actions on the files tab and on the document preview;
|
|
12
|
+
* 5. a FileKit settings page under Settings.
|
|
13
|
+
*
|
|
14
|
+
* All data and mutations go through the Host's loopback route
|
|
15
|
+
* `/dsh-filekit/api`; this bundle imports nothing from Harness Client packages
|
|
16
|
+
* and only uses `--dsw-*` theme tokens.
|
|
17
|
+
*
|
|
18
|
+
* The whole file is an IIFE because client bundles share one global lexical
|
|
19
|
+
* scope in the page and a re-executed bundle would otherwise throw
|
|
20
|
+
* `Identifier 'MODULE_ID' has already been declared`.
|
|
21
|
+
*/
|
|
22
|
+
(() => {
|
|
23
|
+
const MODULE_ID = '@restregular/dsh-filekit';
|
|
24
|
+
const OWNER = 'dsh-filekit';
|
|
25
|
+
const NS = 'dsh-filekit';
|
|
26
|
+
const API = '/dsh-filekit/api';
|
|
27
|
+
const DSH_TRASH = '.dsh-trash';
|
|
28
|
+
|
|
29
|
+
/** Extension identity for the shipped files tab kind. */
|
|
30
|
+
const FILES_ID = '@restregular/dsh-filekit/files';
|
|
31
|
+
/** Identity of the FileKit page type and of its body cell. */
|
|
32
|
+
const FILEKIT_ID = '@restregular/dsh-filekit';
|
|
33
|
+
const FILES_KIND = 'files';
|
|
34
|
+
const FILEKIT_KIND = 'filekit';
|
|
35
|
+
|
|
36
|
+
const factories = window.__ModuleLoader__?.factories;
|
|
37
|
+
if (factories !== undefined && typeof factories.has === 'function' && factories.has(MODULE_ID)) return;
|
|
38
|
+
|
|
39
|
+
window.__ModuleLoader__.load({
|
|
40
|
+
id: MODULE_ID,
|
|
41
|
+
factory(require) {
|
|
42
|
+
const React = require('react');
|
|
43
|
+
const h = React.createElement;
|
|
44
|
+
|
|
45
|
+
// Harness primitives give the editor the same Shiki highlighter the
|
|
46
|
+
// built-in document preview uses. Optional: a missing module degrades to
|
|
47
|
+
// a plain textarea instead of breaking the whole plugin.
|
|
48
|
+
let primitives = undefined;
|
|
49
|
+
try {
|
|
50
|
+
primitives = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
51
|
+
} catch {
|
|
52
|
+
primitives = undefined;
|
|
53
|
+
}
|
|
54
|
+
const useCodeHighlighter =
|
|
55
|
+
typeof primitives?.useCodeHighlighter === 'function' ? primitives.useCodeHighlighter : () => () => undefined;
|
|
56
|
+
const languageForPath =
|
|
57
|
+
typeof primitives?.languageForPath === 'function' ? primitives.languageForPath : () => undefined;
|
|
58
|
+
|
|
59
|
+
// ------------------------------------------------------------------
|
|
60
|
+
// Copy
|
|
61
|
+
// ------------------------------------------------------------------
|
|
62
|
+
const DICT = {
|
|
63
|
+
zh: {
|
|
64
|
+
metaTitle: 'FileKit 文件工具',
|
|
65
|
+
metaDescription: '在右侧「工作区文件」面板中右键操作、拖拽移动、编辑与可恢复删除工作区文件,并把变更自动告知 Agent。',
|
|
66
|
+
filesTitle: '文件',
|
|
67
|
+
filesGuideTitle: '工作区文件',
|
|
68
|
+
filesGuideDescription: '浏览、右键操作与拖拽整理工作区文件',
|
|
69
|
+
guideExtra: 'FileKit 文件工具',
|
|
70
|
+
guideExtraHint: '编辑、回收站与完整文件管理',
|
|
71
|
+
open: '更多操作',
|
|
72
|
+
openTitle: '打开 FileKit 文件工具',
|
|
73
|
+
openFiles: '文件面板',
|
|
74
|
+
openInFileKit: '在 FileKit 中打开',
|
|
75
|
+
openDirInFileKit: '在 FileKit 中浏览',
|
|
76
|
+
title: 'FileKit',
|
|
77
|
+
subtitle: '工作区文件管理器',
|
|
78
|
+
close: '关闭',
|
|
79
|
+
refresh: '刷新',
|
|
80
|
+
retry: '重试',
|
|
81
|
+
back: '返回',
|
|
82
|
+
loading: '正在读取…',
|
|
83
|
+
empty: '这个目录是空的',
|
|
84
|
+
trashEmpty: '回收站是空的',
|
|
85
|
+
newFile: '新建文件',
|
|
86
|
+
newDir: '新建文件夹',
|
|
87
|
+
newFileHere: '在此新建文件',
|
|
88
|
+
newDirHere: '在此新建文件夹',
|
|
89
|
+
trash: '回收站',
|
|
90
|
+
openTrash: '打开回收站',
|
|
91
|
+
edit: '编辑',
|
|
92
|
+
openEditor: '用 FileKit 编辑',
|
|
93
|
+
openPreview: '打开预览',
|
|
94
|
+
rename: '重命名',
|
|
95
|
+
move: '移动到…',
|
|
96
|
+
copy: '复制',
|
|
97
|
+
cut: '剪切',
|
|
98
|
+
paste: '粘贴',
|
|
99
|
+
pasteHere: '粘贴到此处',
|
|
100
|
+
copyRel: '复制相对路径',
|
|
101
|
+
copyAbs: '复制绝对路径',
|
|
102
|
+
reference: '引用到对话框',
|
|
103
|
+
delete: '删除到回收站',
|
|
104
|
+
restore: '恢复',
|
|
105
|
+
purge: '永久删除',
|
|
106
|
+
save: '保存',
|
|
107
|
+
saving: '保存中…',
|
|
108
|
+
cancel: '取消',
|
|
109
|
+
confirm: '确认',
|
|
110
|
+
create: '创建',
|
|
111
|
+
name: '名称',
|
|
112
|
+
newName: '新名称',
|
|
113
|
+
moveTo: '移动到',
|
|
114
|
+
moveHere: '移动到这里',
|
|
115
|
+
chooseDir: '选择目标文件夹',
|
|
116
|
+
noSubdirs: '没有子文件夹',
|
|
117
|
+
parentDir: '上一级',
|
|
118
|
+
deleteTitle: '删除到回收站?',
|
|
119
|
+
deleteConfirm: '删除',
|
|
120
|
+
deleteHint: '条目会移动到工作区的 {trash} 目录,之后可以从回收站恢复。',
|
|
121
|
+
purgeTitle: '永久删除?',
|
|
122
|
+
purgeConfirm: '永久删除',
|
|
123
|
+
purgeHint: '这会直接移除回收站条目,无法恢复。',
|
|
124
|
+
emptyTitle: '清空回收站?',
|
|
125
|
+
emptyConfirm: '清空',
|
|
126
|
+
emptyHint: '回收站里的所有条目都会被永久删除,无法恢复。',
|
|
127
|
+
discardTitle: '放弃未保存的修改?',
|
|
128
|
+
discardConfirm: '放弃修改',
|
|
129
|
+
dirty: '未保存',
|
|
130
|
+
discard: '放弃修改',
|
|
131
|
+
binary: '这是二进制文件,不能用文本编辑器打开。',
|
|
132
|
+
truncated: '文件超过编辑器上限,已只读截断显示。',
|
|
133
|
+
truncatedEntries: '条目太多,只显示了一部分。',
|
|
134
|
+
noChanges: '没有需要保存的修改',
|
|
135
|
+
saved: '已保存',
|
|
136
|
+
created: '已创建',
|
|
137
|
+
renamed: '已重命名',
|
|
138
|
+
moved: '已移动',
|
|
139
|
+
copied: '已复制',
|
|
140
|
+
cutDone: '已剪切',
|
|
141
|
+
pasted: '已粘贴',
|
|
142
|
+
pastedTo: '已粘贴到 {path}',
|
|
143
|
+
deleted: '已移入回收站',
|
|
144
|
+
restored: '已恢复',
|
|
145
|
+
purged: '已永久删除',
|
|
146
|
+
emptied: '回收站已清空',
|
|
147
|
+
referenced: '已插入引用',
|
|
148
|
+
referencedCopied: '输入框不可用,引用已复制到剪贴板',
|
|
149
|
+
copiedPath: '路径已复制',
|
|
150
|
+
rootLabel: '工作区',
|
|
151
|
+
lines: '行',
|
|
152
|
+
bytes: '字节',
|
|
153
|
+
modified: '修改于',
|
|
154
|
+
conflictHint: '文件在磁盘上已被修改,请刷新后重试。',
|
|
155
|
+
dragHint: '拖拽移动 · 右键操作',
|
|
156
|
+
dropRoot: '移动到工作区根目录',
|
|
157
|
+
moving: '正在移动…',
|
|
158
|
+
saveBeforeClose: '还有未保存的修改,请先保存或放弃修改。',
|
|
159
|
+
saveBeforeSwitch: '还有未保存的修改,请先保存或放弃修改。',
|
|
160
|
+
settingsTitle: 'FileKit',
|
|
161
|
+
settingsIntro: '这些设置只影响本机的 FileKit 界面。',
|
|
162
|
+
settingsShowHidden: '显示隐藏文件',
|
|
163
|
+
settingsShowHiddenHint: '在文件树和 FileKit 中显示以 . 开头的文件与目录。',
|
|
164
|
+
settingsNotify: '变更后提醒 Agent',
|
|
165
|
+
settingsNotifyHint: '关闭后不再向当前会话注入变更提醒,可节省上下文 token。',
|
|
166
|
+
settingsConfirmDelete: '删除前确认',
|
|
167
|
+
settingsConfirmDeleteHint: '关闭后「删除」会直接把条目移入工作区回收站。',
|
|
168
|
+
settingsAutoRefresh: '自动刷新',
|
|
169
|
+
settingsAutoRefreshHint: '定期重新读取已经展开的目录,发现外部改动。',
|
|
170
|
+
settingsAutoRefreshOff: '关闭',
|
|
171
|
+
settingsAutoRefresh15: '15 秒',
|
|
172
|
+
settingsAutoRefresh30: '30 秒',
|
|
173
|
+
settingsAutoRefresh60: '60 秒',
|
|
174
|
+
settingsFontSize: '编辑器字号',
|
|
175
|
+
settingsFontSizeHint: 'FileKit 内置文本编辑器的字号。',
|
|
176
|
+
settingsReset: '恢复默认设置',
|
|
177
|
+
settingsResetDone: '已恢复默认设置',
|
|
178
|
+
settingsStorageFail: '设置无法保存到浏览器存储。',
|
|
179
|
+
errorPrefix: '操作失败:',
|
|
180
|
+
'error.bad-path': '路径无效。',
|
|
181
|
+
'error.bad-name': '名称无效。',
|
|
182
|
+
'error.bad-root': '工作区路径无效。',
|
|
183
|
+
'error.no-root': '工作区目录不存在。',
|
|
184
|
+
'error.unknown-workspace': '这个目录不是已注册的工作区。',
|
|
185
|
+
'error.no-workspace': '无法确定工作区目录。',
|
|
186
|
+
'error.no-session': '当前没有可用的会话。',
|
|
187
|
+
'error.forbidden': '请求被拒绝。',
|
|
188
|
+
'error.not-found': '条目不存在。',
|
|
189
|
+
'error.not-directory': '这不是一个目录。',
|
|
190
|
+
'error.not-file': '这不是一个文件。',
|
|
191
|
+
'error.exists': '同名条目已存在。',
|
|
192
|
+
'error.parent-missing': '上级目录不存在。',
|
|
193
|
+
'error.outside-workspace': '路径超出了工作区范围。',
|
|
194
|
+
'error.symlink': '不允许通过符号链接操作。',
|
|
195
|
+
'error.protected': '这个位置受保护,不能这样操作。',
|
|
196
|
+
'error.bad-move': '不能把目录移动到它自己里面。',
|
|
197
|
+
'error.bad-copy': '不能把目录复制到它自己里面。',
|
|
198
|
+
'error.too-large': '内容太大。',
|
|
199
|
+
'error.conflict': '文件已被其他程序修改,请刷新后再保存。',
|
|
200
|
+
'error.bad-entry': '回收站条目无效。',
|
|
201
|
+
'error.bad-input': '请求参数无效。',
|
|
202
|
+
'error.bad-action': '未知操作。',
|
|
203
|
+
'error.unknown-op': '未知操作。',
|
|
204
|
+
'error.internal': '内部错误。',
|
|
205
|
+
},
|
|
206
|
+
en: {
|
|
207
|
+
metaTitle: 'FileKit file tools',
|
|
208
|
+
metaDescription: 'Right-click, drag, edit, and recoverably delete workspace files from the right-sidebar workspace files panel, with changes reported to the Agent.',
|
|
209
|
+
filesTitle: 'Files',
|
|
210
|
+
filesGuideTitle: 'Workspace files',
|
|
211
|
+
filesGuideDescription: 'Browse, right-click, and drag to organize workspace files',
|
|
212
|
+
guideExtra: 'FileKit file manager',
|
|
213
|
+
guideExtraHint: 'Editor, recycle bin, and full file management',
|
|
214
|
+
open: 'More actions',
|
|
215
|
+
openTitle: 'Open FileKit',
|
|
216
|
+
openFiles: 'Files panel',
|
|
217
|
+
openInFileKit: 'Open in FileKit',
|
|
218
|
+
openDirInFileKit: 'Browse in FileKit',
|
|
219
|
+
title: 'FileKit',
|
|
220
|
+
subtitle: 'Workspace file manager',
|
|
221
|
+
close: 'Close',
|
|
222
|
+
refresh: 'Refresh',
|
|
223
|
+
retry: 'Retry',
|
|
224
|
+
back: 'Back',
|
|
225
|
+
loading: 'Loading…',
|
|
226
|
+
empty: 'This directory is empty',
|
|
227
|
+
trashEmpty: 'The recycle bin is empty',
|
|
228
|
+
newFile: 'New file',
|
|
229
|
+
newDir: 'New folder',
|
|
230
|
+
newFileHere: 'New file here',
|
|
231
|
+
newDirHere: 'New folder here',
|
|
232
|
+
trash: 'Recycle bin',
|
|
233
|
+
openTrash: 'Open recycle bin',
|
|
234
|
+
edit: 'Edit',
|
|
235
|
+
openEditor: 'Edit in FileKit',
|
|
236
|
+
openPreview: 'Open preview',
|
|
237
|
+
rename: 'Rename',
|
|
238
|
+
move: 'Move to…',
|
|
239
|
+
copy: 'Copy',
|
|
240
|
+
cut: 'Cut',
|
|
241
|
+
paste: 'Paste',
|
|
242
|
+
pasteHere: 'Paste here',
|
|
243
|
+
copyRel: 'Copy relative path',
|
|
244
|
+
copyAbs: 'Copy absolute path',
|
|
245
|
+
reference: 'Reference in composer',
|
|
246
|
+
delete: 'Move to recycle bin',
|
|
247
|
+
restore: 'Restore',
|
|
248
|
+
purge: 'Delete permanently',
|
|
249
|
+
save: 'Save',
|
|
250
|
+
saving: 'Saving…',
|
|
251
|
+
cancel: 'Cancel',
|
|
252
|
+
confirm: 'Confirm',
|
|
253
|
+
create: 'Create',
|
|
254
|
+
name: 'Name',
|
|
255
|
+
newName: 'New name',
|
|
256
|
+
moveTo: 'Move to',
|
|
257
|
+
moveHere: 'Move here',
|
|
258
|
+
chooseDir: 'Choose a destination folder',
|
|
259
|
+
noSubdirs: 'No subfolders',
|
|
260
|
+
parentDir: 'Parent',
|
|
261
|
+
deleteTitle: 'Move to the recycle bin?',
|
|
262
|
+
deleteConfirm: 'Move to trash',
|
|
263
|
+
deleteHint: 'The entry moves into the workspace {trash} folder and can be restored from the recycle bin.',
|
|
264
|
+
purgeTitle: 'Delete permanently?',
|
|
265
|
+
purgeConfirm: 'Delete permanently',
|
|
266
|
+
purgeHint: 'This removes the trash entry and cannot be undone.',
|
|
267
|
+
emptyTitle: 'Empty the recycle bin?',
|
|
268
|
+
emptyConfirm: 'Empty',
|
|
269
|
+
emptyHint: 'Every entry in the recycle bin is permanently deleted; this cannot be undone.',
|
|
270
|
+
discardTitle: 'Discard unsaved changes?',
|
|
271
|
+
discardConfirm: 'Discard',
|
|
272
|
+
dirty: 'Unsaved',
|
|
273
|
+
discard: 'Discard',
|
|
274
|
+
binary: 'This is a binary file; the text editor cannot open it.',
|
|
275
|
+
truncated: 'This file exceeds the editor limit and is shown read-only.',
|
|
276
|
+
truncatedEntries: 'Too many entries, showing only some of them.',
|
|
277
|
+
noChanges: 'Nothing to save',
|
|
278
|
+
saved: 'Saved',
|
|
279
|
+
created: 'Created',
|
|
280
|
+
renamed: 'Renamed',
|
|
281
|
+
moved: 'Moved',
|
|
282
|
+
copied: 'Copied',
|
|
283
|
+
cutDone: 'Cut',
|
|
284
|
+
pasted: 'Pasted',
|
|
285
|
+
pastedTo: 'Pasted into {path}',
|
|
286
|
+
deleted: 'Moved to the recycle bin',
|
|
287
|
+
restored: 'Restored',
|
|
288
|
+
purged: 'Permanently deleted',
|
|
289
|
+
emptied: 'Recycle bin emptied',
|
|
290
|
+
referenced: 'Reference inserted',
|
|
291
|
+
referencedCopied: 'Composer unavailable; reference copied to the clipboard',
|
|
292
|
+
copiedPath: 'Path copied',
|
|
293
|
+
rootLabel: 'Workspace',
|
|
294
|
+
lines: 'lines',
|
|
295
|
+
bytes: 'bytes',
|
|
296
|
+
modified: 'Modified',
|
|
297
|
+
conflictHint: 'The file changed on disk. Refresh and try again.',
|
|
298
|
+
dragHint: 'Drag to move · Right-click for actions',
|
|
299
|
+
dropRoot: 'Move to the workspace root',
|
|
300
|
+
moving: 'Moving…',
|
|
301
|
+
saveBeforeClose: 'Unsaved changes: save or discard them before closing.',
|
|
302
|
+
saveBeforeSwitch: 'Unsaved changes: save or discard them first.',
|
|
303
|
+
settingsTitle: 'FileKit',
|
|
304
|
+
settingsIntro: 'These settings only affect FileKit in this browser.',
|
|
305
|
+
settingsShowHidden: 'Show hidden files',
|
|
306
|
+
settingsShowHiddenHint: 'Show files and folders whose names start with a dot.',
|
|
307
|
+
settingsNotify: 'Tell the Agent after changes',
|
|
308
|
+
settingsNotifyHint: 'Off stops injecting workspace-change notices into the current session, saving context tokens.',
|
|
309
|
+
settingsConfirmDelete: 'Confirm before deleting',
|
|
310
|
+
settingsConfirmDeleteHint: 'Off moves entries to the workspace recycle bin without a dialog.',
|
|
311
|
+
settingsAutoRefresh: 'Auto refresh',
|
|
312
|
+
settingsAutoRefreshHint: 'Periodically reread expanded directories to pick up outside changes.',
|
|
313
|
+
settingsAutoRefreshOff: 'Off',
|
|
314
|
+
settingsAutoRefresh15: '15 s',
|
|
315
|
+
settingsAutoRefresh30: '30 s',
|
|
316
|
+
settingsAutoRefresh60: '60 s',
|
|
317
|
+
settingsFontSize: 'Editor font size',
|
|
318
|
+
settingsFontSizeHint: 'Font size of the built-in text editor.',
|
|
319
|
+
settingsReset: 'Restore defaults',
|
|
320
|
+
settingsResetDone: 'Defaults restored',
|
|
321
|
+
settingsStorageFail: 'The settings could not be saved in browser storage.',
|
|
322
|
+
errorPrefix: 'Operation failed: ',
|
|
323
|
+
'error.bad-path': 'Invalid path.',
|
|
324
|
+
'error.bad-name': 'Invalid name.',
|
|
325
|
+
'error.bad-root': 'Invalid workspace path.',
|
|
326
|
+
'error.no-root': 'The workspace directory does not exist.',
|
|
327
|
+
'error.unknown-workspace': 'This directory is not a registered workspace.',
|
|
328
|
+
'error.no-workspace': 'Could not resolve a workspace directory.',
|
|
329
|
+
'error.no-session': 'No live session is available.',
|
|
330
|
+
'error.forbidden': 'The request was rejected.',
|
|
331
|
+
'error.not-found': 'The entry does not exist.',
|
|
332
|
+
'error.not-directory': 'This is not a directory.',
|
|
333
|
+
'error.not-file': 'This is not a file.',
|
|
334
|
+
'error.exists': 'An entry with that name already exists.',
|
|
335
|
+
'error.parent-missing': 'The parent directory does not exist.',
|
|
336
|
+
'error.outside-workspace': 'The path escapes the workspace.',
|
|
337
|
+
'error.symlink': 'Symbolic-link operations are not allowed.',
|
|
338
|
+
'error.protected': 'That location is protected.',
|
|
339
|
+
'error.bad-move': 'A directory cannot be moved into itself.',
|
|
340
|
+
'error.bad-copy': 'A directory cannot be copied into itself.',
|
|
341
|
+
'error.too-large': 'The content is too large.',
|
|
342
|
+
'error.conflict': 'The file changed on disk; refresh before saving.',
|
|
343
|
+
'error.bad-entry': 'Invalid trash entry.',
|
|
344
|
+
'error.bad-input': 'Invalid request.',
|
|
345
|
+
'error.bad-action': 'Unknown action.',
|
|
346
|
+
'error.unknown-op': 'Unknown operation.',
|
|
347
|
+
'error.internal': 'Internal error.',
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/** Fallback translator used before the locale service binds. */
|
|
352
|
+
function fallbackText(key) {
|
|
353
|
+
return DICT.zh[key] ?? DICT.en[key] ?? key;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let translate = fallbackText;
|
|
357
|
+
|
|
358
|
+
/** Translate one key, replacing `{name}` placeholders. */
|
|
359
|
+
function t(key, params) {
|
|
360
|
+
let text = translate(key);
|
|
361
|
+
if (params !== undefined && params !== null) {
|
|
362
|
+
for (const [name, value] of Object.entries(params)) {
|
|
363
|
+
text = text.split('{' + name + '}').join(String(value));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return text;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Stable, localized error text for a Host failure. */
|
|
370
|
+
function errorText(error) {
|
|
371
|
+
const code = error?.code;
|
|
372
|
+
if (typeof code === 'string') {
|
|
373
|
+
const key = 'error.' + code;
|
|
374
|
+
const localized = translate(key);
|
|
375
|
+
if (localized !== key) return localized;
|
|
376
|
+
}
|
|
377
|
+
return String(error?.message ?? error ?? translate('error.internal'));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ------------------------------------------------------------------
|
|
381
|
+
// Persistent settings
|
|
382
|
+
// ------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
const SETTINGS_KEY = 'dsh-filekit.settings.v1';
|
|
385
|
+
const DEFAULT_SETTINGS = Object.freeze({
|
|
386
|
+
showHidden: true,
|
|
387
|
+
notifyAgent: true,
|
|
388
|
+
confirmDelete: true,
|
|
389
|
+
autoRefreshSeconds: 0,
|
|
390
|
+
editorFontSize: 12,
|
|
391
|
+
});
|
|
392
|
+
const AUTO_REFRESH_CHOICES = [0, 15, 30, 60];
|
|
393
|
+
const FONT_MIN = 11;
|
|
394
|
+
const FONT_MAX = 18;
|
|
395
|
+
|
|
396
|
+
function clampFontSize(value) {
|
|
397
|
+
const number = Number(value);
|
|
398
|
+
if (!Number.isFinite(number)) return DEFAULT_SETTINGS.editorFontSize;
|
|
399
|
+
return Math.min(FONT_MAX, Math.max(FONT_MIN, Math.round(number)));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function normalizeAutoRefresh(value) {
|
|
403
|
+
const number = Number(value);
|
|
404
|
+
return AUTO_REFRESH_CHOICES.includes(number) ? number : DEFAULT_SETTINGS.autoRefreshSeconds;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function readSettings() {
|
|
408
|
+
const next = { ...DEFAULT_SETTINGS };
|
|
409
|
+
try {
|
|
410
|
+
const raw = window.localStorage.getItem(SETTINGS_KEY);
|
|
411
|
+
if (raw === null) return next;
|
|
412
|
+
const parsed = JSON.parse(raw);
|
|
413
|
+
if (typeof parsed !== 'object' || parsed === null) return next;
|
|
414
|
+
if (typeof parsed.showHidden === 'boolean') next.showHidden = parsed.showHidden;
|
|
415
|
+
if (typeof parsed.notifyAgent === 'boolean') next.notifyAgent = parsed.notifyAgent;
|
|
416
|
+
if (typeof parsed.confirmDelete === 'boolean') next.confirmDelete = parsed.confirmDelete;
|
|
417
|
+
next.autoRefreshSeconds = normalizeAutoRefresh(parsed.autoRefreshSeconds);
|
|
418
|
+
next.editorFontSize = clampFontSize(parsed.editorFontSize);
|
|
419
|
+
} catch {
|
|
420
|
+
/* unusable storage falls back to defaults */
|
|
421
|
+
}
|
|
422
|
+
return next;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let settings = readSettings();
|
|
426
|
+
const settingsListeners = new Set();
|
|
427
|
+
|
|
428
|
+
function emitSettings() {
|
|
429
|
+
for (const listener of settingsListeners) {
|
|
430
|
+
try {
|
|
431
|
+
listener();
|
|
432
|
+
} catch {
|
|
433
|
+
/* a broken listener must not break the others */
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function writeSettings(next) {
|
|
439
|
+
settings = next;
|
|
440
|
+
try {
|
|
441
|
+
window.localStorage.setItem(SETTINGS_KEY, JSON.stringify(next));
|
|
442
|
+
} catch {
|
|
443
|
+
console.warn('[dsh-filekit] settings could not be persisted');
|
|
444
|
+
}
|
|
445
|
+
emitSettings();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function updateSettings(patch) {
|
|
449
|
+
writeSettings({ ...settings, ...patch });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function resetSettings() {
|
|
453
|
+
writeSettings({ ...DEFAULT_SETTINGS });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function useSettings() {
|
|
457
|
+
const [snapshot, setSnapshot] = React.useState(settings);
|
|
458
|
+
React.useEffect(() => {
|
|
459
|
+
const update = () => setSnapshot(settings);
|
|
460
|
+
settingsListeners.add(update);
|
|
461
|
+
return () => settingsListeners.delete(update);
|
|
462
|
+
}, []);
|
|
463
|
+
return snapshot;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ------------------------------------------------------------------
|
|
467
|
+
// Host API
|
|
468
|
+
// ------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
async function api(op, payload = {}) {
|
|
471
|
+
const response = await fetch(API, {
|
|
472
|
+
method: 'POST',
|
|
473
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
474
|
+
body: JSON.stringify({ op, source: 'ui', notify: settings.notifyAgent, ...payload }),
|
|
475
|
+
});
|
|
476
|
+
const data = await response.json().catch(() => null);
|
|
477
|
+
if (data === null) {
|
|
478
|
+
const error = new Error(`HTTP ${response.status}`);
|
|
479
|
+
error.code = 'internal';
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
if (data.ok !== true) {
|
|
483
|
+
const error = new Error(typeof data.error === 'string' ? data.error : `HTTP ${response.status}`);
|
|
484
|
+
error.code = typeof data.code === 'string' ? data.code : 'internal';
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
return data;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ------------------------------------------------------------------
|
|
491
|
+
// Paths and clipboard
|
|
492
|
+
// ------------------------------------------------------------------
|
|
493
|
+
|
|
494
|
+
const processPlatform = typeof navigator !== 'undefined' && /Win/iu.test(navigator.userAgent) ? 'win32' : 'other';
|
|
495
|
+
|
|
496
|
+
function toSlashes(value) {
|
|
497
|
+
return String(value).replace(/\\/gu, '/');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function trimRoot(value) {
|
|
501
|
+
const text = toSlashes(value);
|
|
502
|
+
return text.length > 1 ? text.replace(/\/+$/u, '') : text;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function relFromAbs(abs, root) {
|
|
506
|
+
if (typeof abs !== 'string' || typeof root !== 'string') return null;
|
|
507
|
+
const a = trimRoot(abs);
|
|
508
|
+
const r = trimRoot(root);
|
|
509
|
+
const lower = (value) => (processPlatform === 'win32' ? value.toLowerCase() : value);
|
|
510
|
+
if (lower(a) === lower(r)) return '';
|
|
511
|
+
if (lower(a).startsWith(lower(r) + '/')) return a.slice(r.length + 1);
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function dirOf(rel) {
|
|
516
|
+
const index = rel.lastIndexOf('/');
|
|
517
|
+
return index < 0 ? '' : rel.slice(0, index);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function baseOf(rel) {
|
|
521
|
+
const index = rel.lastIndexOf('/');
|
|
522
|
+
return index < 0 ? rel : rel.slice(index + 1);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function joinRel(dir, name) {
|
|
526
|
+
return dir === '' ? name : dir + '/' + name;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function joinAbs(root, rel) {
|
|
530
|
+
const clean = String(root).replace(/[\\/]+$/u, '');
|
|
531
|
+
const sep = clean.includes('\\') ? '\\' : '/';
|
|
532
|
+
return rel === '' ? clean : clean + sep + rel.split('/').join(sep);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function workspaceName(root) {
|
|
536
|
+
const parts = toSlashes(root).split('/').filter((part) => part !== '');
|
|
537
|
+
return parts.length === 0 ? toSlashes(root) : parts[parts.length - 1];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Address of a workspace file, as the sidebar's preview tab expects it. */
|
|
541
|
+
function fileAddress(sessionId, rel) {
|
|
542
|
+
const id = encodeURIComponent(String(sessionId));
|
|
543
|
+
return 'dsh-resource://file/session/' + id + '/' + rel.split('/').map((part) => encodeURIComponent(part)).join('/');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function formatBytes(size) {
|
|
547
|
+
if (typeof size !== 'number' || !Number.isFinite(size)) return '';
|
|
548
|
+
if (size < 1024) return `${size} B`;
|
|
549
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
|
550
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isHiddenEntry(name) {
|
|
554
|
+
return settings.showHidden !== true && String(name).startsWith('.');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function copyText(text) {
|
|
558
|
+
try {
|
|
559
|
+
if (navigator.clipboard !== undefined) {
|
|
560
|
+
await navigator.clipboard.writeText(text);
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
} catch {
|
|
564
|
+
/* fall through to the legacy path */
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const area = document.createElement('textarea');
|
|
568
|
+
area.value = text;
|
|
569
|
+
area.setAttribute('readonly', '');
|
|
570
|
+
area.style.position = 'fixed';
|
|
571
|
+
area.style.left = '-9999px';
|
|
572
|
+
area.style.top = '0';
|
|
573
|
+
document.body.appendChild(area);
|
|
574
|
+
area.select();
|
|
575
|
+
const ok = document.execCommand('copy');
|
|
576
|
+
area.remove();
|
|
577
|
+
return ok;
|
|
578
|
+
} catch {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ------------------------------------------------------------------
|
|
584
|
+
// Toast and style ownership
|
|
585
|
+
// ------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
let toastSpec = null;
|
|
588
|
+
const toastListeners = new Set();
|
|
589
|
+
let toastTimer = undefined;
|
|
590
|
+
|
|
591
|
+
function emitToast() {
|
|
592
|
+
for (const listener of toastListeners) {
|
|
593
|
+
try {
|
|
594
|
+
listener();
|
|
595
|
+
} catch {
|
|
596
|
+
/* ignore */
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function showToast(text, kind = 'ok') {
|
|
602
|
+
toastSpec = { text, kind, key: Date.now() };
|
|
603
|
+
emitToast();
|
|
604
|
+
if (toastTimer !== undefined) clearTimeout(toastTimer);
|
|
605
|
+
toastTimer = setTimeout(() => {
|
|
606
|
+
toastSpec = null;
|
|
607
|
+
toastTimer = undefined;
|
|
608
|
+
emitToast();
|
|
609
|
+
}, 4200);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function useToastSpec() {
|
|
613
|
+
const [spec, setSpec] = React.useState(toastSpec);
|
|
614
|
+
React.useEffect(() => {
|
|
615
|
+
const listener = () => setSpec(toastSpec);
|
|
616
|
+
toastListeners.add(listener);
|
|
617
|
+
return () => toastListeners.delete(listener);
|
|
618
|
+
}, []);
|
|
619
|
+
return spec;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function ToastHost() {
|
|
623
|
+
const spec = useToastSpec();
|
|
624
|
+
if (spec === null) return null;
|
|
625
|
+
return h('div', { className: 'dsh-fk-toast', 'data-kind': spec.kind, role: 'status' }, spec.text);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ------------------------------------------------------------------
|
|
629
|
+
// File clipboard (copy / cut → paste)
|
|
630
|
+
// ------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
/** Shared across both panels: copy in one, paste in the other. */
|
|
633
|
+
let fileClipboard = null;
|
|
634
|
+
const clipboardListeners = new Set();
|
|
635
|
+
|
|
636
|
+
function setFileClipboard(value) {
|
|
637
|
+
fileClipboard = value;
|
|
638
|
+
for (const listener of clipboardListeners) {
|
|
639
|
+
try {
|
|
640
|
+
listener();
|
|
641
|
+
} catch {
|
|
642
|
+
/* ignore */
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function useFileClipboard() {
|
|
648
|
+
const [value, setValue] = React.useState(fileClipboard);
|
|
649
|
+
React.useEffect(() => {
|
|
650
|
+
const listener = () => setValue(fileClipboard);
|
|
651
|
+
clipboardListeners.add(listener);
|
|
652
|
+
return () => clipboardListeners.delete(listener);
|
|
653
|
+
}, []);
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Put one entry on the FileKit clipboard (no filesystem work yet). */
|
|
658
|
+
function copyToFileClipboard(root, rel, kind, mode) {
|
|
659
|
+
setFileClipboard({ mode, root, rel, kind, name: baseOf(rel) });
|
|
660
|
+
showToast(mode === 'cut' ? t('cutDone') : t('copied'), 'ok');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ------------------------------------------------------------------
|
|
664
|
+
// Highlighted code editor
|
|
665
|
+
// ------------------------------------------------------------------
|
|
666
|
+
|
|
667
|
+
/** Above this size the editor skips highlighting so typing stays fluid. */
|
|
668
|
+
const HIGHLIGHT_MAX_CHARS = 200_000;
|
|
669
|
+
|
|
670
|
+
function CodeEditor({ value, onChange, rel, fontSize, readOnly = false }) {
|
|
671
|
+
const inputRef = React.useRef(null);
|
|
672
|
+
const preRef = React.useRef(null);
|
|
673
|
+
const [composing, setComposing] = React.useState(false);
|
|
674
|
+
const deferred = React.useDeferredValue(value);
|
|
675
|
+
const language = React.useMemo(() => languageForPath(rel), [rel]);
|
|
676
|
+
const highlight = useCodeHighlighter(language);
|
|
677
|
+
const plainLines = React.useMemo(() => String(deferred).split('\n'), [deferred]);
|
|
678
|
+
const lines = React.useMemo(() => {
|
|
679
|
+
if (typeof highlight !== 'function' || deferred.length > HIGHLIGHT_MAX_CHARS) return undefined;
|
|
680
|
+
try {
|
|
681
|
+
const result = highlight(deferred);
|
|
682
|
+
return Array.isArray(result) ? result : undefined;
|
|
683
|
+
} catch {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
}, [deferred, highlight]);
|
|
687
|
+
const totalLines = String(value).split('\n').length;
|
|
688
|
+
const rendered = [];
|
|
689
|
+
const source = lines ?? plainLines;
|
|
690
|
+
for (let index = 0; index < Math.max(source.length, totalLines); index += 1) {
|
|
691
|
+
const line = source[index];
|
|
692
|
+
rendered.push(
|
|
693
|
+
h(
|
|
694
|
+
'div',
|
|
695
|
+
{ className: 'dsh-fk-codeeditor-line', key: index },
|
|
696
|
+
Array.isArray(line)
|
|
697
|
+
? line.map((token, tokenIndex) => h('span', { key: tokenIndex, style: token.style }, token.text))
|
|
698
|
+
: line ?? '',
|
|
699
|
+
),
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const syncScroll = (event) => {
|
|
704
|
+
const pre = preRef.current;
|
|
705
|
+
if (pre === null) return;
|
|
706
|
+
pre.scrollTop = event.currentTarget.scrollTop;
|
|
707
|
+
pre.scrollLeft = event.currentTarget.scrollLeft;
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// After edits (or a highlight pass) the textarea may have scrolled under
|
|
711
|
+
// the caret; keep the painted layer on the same scroll offset.
|
|
712
|
+
React.useEffect(() => {
|
|
713
|
+
const input = inputRef.current;
|
|
714
|
+
const pre = preRef.current;
|
|
715
|
+
if (input === null || pre === null) return;
|
|
716
|
+
pre.scrollTop = input.scrollTop;
|
|
717
|
+
pre.scrollLeft = input.scrollLeft;
|
|
718
|
+
}, [value, lines]);
|
|
719
|
+
|
|
720
|
+
return h(
|
|
721
|
+
'div',
|
|
722
|
+
{ className: 'dsh-fk-codeeditor' },
|
|
723
|
+
h(
|
|
724
|
+
'pre',
|
|
725
|
+
{
|
|
726
|
+
className: 'dsh-fk-codeeditor-pre',
|
|
727
|
+
ref: preRef,
|
|
728
|
+
'aria-hidden': true,
|
|
729
|
+
style: { fontSize: `${fontSize}px`, visibility: composing ? 'hidden' : 'visible' },
|
|
730
|
+
},
|
|
731
|
+
rendered,
|
|
732
|
+
),
|
|
733
|
+
h('textarea', {
|
|
734
|
+
ref: inputRef,
|
|
735
|
+
className: 'dsh-fk-codeeditor-input',
|
|
736
|
+
value,
|
|
737
|
+
spellCheck: false,
|
|
738
|
+
readOnly,
|
|
739
|
+
wrap: 'off',
|
|
740
|
+
autoCapitalize: 'off',
|
|
741
|
+
autoCorrect: 'off',
|
|
742
|
+
autoComplete: 'off',
|
|
743
|
+
style: {
|
|
744
|
+
fontSize: `${fontSize}px`,
|
|
745
|
+
// IME composition text would be invisible through the transparent
|
|
746
|
+
// layer; show it plainly while the candidate window is open.
|
|
747
|
+
color: composing ? 'var(--dsw-alias-label-primary)' : 'transparent',
|
|
748
|
+
},
|
|
749
|
+
onChange: (event) => onChange(event.target.value),
|
|
750
|
+
onScroll: syncScroll,
|
|
751
|
+
onCompositionStart: () => setComposing(true),
|
|
752
|
+
onCompositionEnd: () => setComposing(false),
|
|
753
|
+
onKeyDown: (event) => {
|
|
754
|
+
if (readOnly || event.key !== 'Tab') return;
|
|
755
|
+
event.preventDefault();
|
|
756
|
+
const area = event.currentTarget;
|
|
757
|
+
const start = area.selectionStart;
|
|
758
|
+
const end = area.selectionEnd;
|
|
759
|
+
onChange(`${value.slice(0, start)} ${value.slice(end)}`);
|
|
760
|
+
window.requestAnimationFrame(() => {
|
|
761
|
+
area.selectionStart = start + 2;
|
|
762
|
+
area.selectionEnd = start + 2;
|
|
763
|
+
});
|
|
764
|
+
},
|
|
765
|
+
}),
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function createOwnedStyle(css) {
|
|
770
|
+
const el = document.createElement('style');
|
|
771
|
+
el.setAttribute('data-plugin', OWNER);
|
|
772
|
+
el.textContent = css;
|
|
773
|
+
document.head.appendChild(el);
|
|
774
|
+
return el;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ------------------------------------------------------------------
|
|
778
|
+
// Dirty tabs (the FileKit page's close guard)
|
|
779
|
+
// ------------------------------------------------------------------
|
|
780
|
+
|
|
781
|
+
const dirtyTabs = new Set();
|
|
782
|
+
|
|
783
|
+
function setTabDirty(tabId, dirty) {
|
|
784
|
+
if (typeof tabId !== 'string' || tabId === '') return;
|
|
785
|
+
if (dirty) dirtyTabs.add(tabId);
|
|
786
|
+
else dirtyTabs.delete(tabId);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// ------------------------------------------------------------------
|
|
790
|
+
// Icons — 24-grid line art; gold/orange-red accents live in CSS
|
|
791
|
+
// ------------------------------------------------------------------
|
|
792
|
+
|
|
793
|
+
function Icon({ name, size = 16, className }) {
|
|
794
|
+
const common = {
|
|
795
|
+
width: size,
|
|
796
|
+
height: size,
|
|
797
|
+
viewBox: '0 0 24 24',
|
|
798
|
+
fill: 'none',
|
|
799
|
+
stroke: 'currentColor',
|
|
800
|
+
strokeWidth: 1.65,
|
|
801
|
+
strokeLinecap: 'round',
|
|
802
|
+
strokeLinejoin: 'round',
|
|
803
|
+
'aria-hidden': true,
|
|
804
|
+
focusable: false,
|
|
805
|
+
className,
|
|
806
|
+
style: { display: 'block', flex: 'none' },
|
|
807
|
+
};
|
|
808
|
+
switch (name) {
|
|
809
|
+
case 'brand':
|
|
810
|
+
case 'folderSpark':
|
|
811
|
+
return h(
|
|
812
|
+
'svg',
|
|
813
|
+
common,
|
|
814
|
+
h('path', { d: 'M3 6.8A2.3 2.3 0 0 1 5.3 4.5h3l2 2h8.4A2.3 2.3 0 0 1 21 8.8v8.4a2.3 2.3 0 0 1-2.3 2.3H5.3A2.3 2.3 0 0 1 3 17.2z' }),
|
|
815
|
+
h('path', { d: 'M15.2 9.6l.75 2.05 2.05.75-2.05.75-.75 2.05-.75-2.05L12.4 12.4l2.05-.75z' }),
|
|
816
|
+
);
|
|
817
|
+
case 'folder':
|
|
818
|
+
return h('svg', common, h('path', { d: 'M3 6.5A2.5 2.5 0 0 1 5.5 4h3.2l2 2h7.8A2.5 2.5 0 0 1 21 8.5v9A2.5 2.5 0 0 1 18.5 20h-13A2.5 2.5 0 0 1 3 17.5z' }));
|
|
819
|
+
case 'folderOpen':
|
|
820
|
+
return h(
|
|
821
|
+
'svg',
|
|
822
|
+
common,
|
|
823
|
+
h('path', { d: 'M4 8V6.5A2.5 2.5 0 0 1 6.5 4h3.2l2 2h6.8A2.5 2.5 0 0 1 21 8.5V9' }),
|
|
824
|
+
h('path', { d: 'M3.6 10h17.1l-2.1 8.2a2 2 0 0 1-1.9 1.3H6.4a2 2 0 0 1-1.9-1.4z' }),
|
|
825
|
+
);
|
|
826
|
+
case 'folderPlus':
|
|
827
|
+
return h(
|
|
828
|
+
'svg',
|
|
829
|
+
common,
|
|
830
|
+
h('path', { d: 'M3 6.5A2.5 2.5 0 0 1 5.5 4h3.2l2 2h7.8A2.5 2.5 0 0 1 21 8.5v9A2.5 2.5 0 0 1 18.5 20h-13A2.5 2.5 0 0 1 3 17.5z' }),
|
|
831
|
+
h('path', { d: 'M12 11v6M9 14h6' }),
|
|
832
|
+
);
|
|
833
|
+
case 'file':
|
|
834
|
+
return h('svg', common, h('path', { d: 'M6.5 3.5h7l4.5 4.5v12.5h-11.5z' }), h('path', { d: 'M13.5 3.5V8H18' }));
|
|
835
|
+
case 'filePlus':
|
|
836
|
+
return h(
|
|
837
|
+
'svg',
|
|
838
|
+
common,
|
|
839
|
+
h('path', { d: 'M6.5 3.5h7l4.5 4.5v12.5h-11.5z' }),
|
|
840
|
+
h('path', { d: 'M13.5 3.5V8H18M12 11.5v5M9.5 14h5' }),
|
|
841
|
+
);
|
|
842
|
+
case 'edit':
|
|
843
|
+
return h('svg', common, h('path', { d: 'M4 20h4L19 9l-4-4L4 16z' }), h('path', { d: 'M13.5 6.5l4 4' }));
|
|
844
|
+
case 'copy':
|
|
845
|
+
return h('svg', common, h('rect', { x: 9, y: 9, width: 11, height: 11, rx: 2 }), h('path', { d: 'M5 15H4V4h11v1' }));
|
|
846
|
+
case 'cut':
|
|
847
|
+
return h(
|
|
848
|
+
'svg',
|
|
849
|
+
common,
|
|
850
|
+
h('circle', { cx: 6.5, cy: 17.5, r: 2.5 }),
|
|
851
|
+
h('circle', { cx: 17.5, cy: 17.5, r: 2.5 }),
|
|
852
|
+
h('path', { d: 'M8.2 15.6L16 3M15.8 15.6L8 3' }),
|
|
853
|
+
);
|
|
854
|
+
case 'paste':
|
|
855
|
+
return h(
|
|
856
|
+
'svg',
|
|
857
|
+
common,
|
|
858
|
+
h('path', { d: 'M9 4.5h6v3H9z' }),
|
|
859
|
+
h('path', { d: 'M9 6H6.5A1.5 1.5 0 0 0 5 7.5v12A1.5 1.5 0 0 0 6.5 21h11a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 17.5 6H15' }),
|
|
860
|
+
h('path', { d: 'M9 12h6M9 16h6' }),
|
|
861
|
+
);
|
|
862
|
+
case 'link':
|
|
863
|
+
return h(
|
|
864
|
+
'svg',
|
|
865
|
+
common,
|
|
866
|
+
h('path', { d: 'M10 13.5a4 4 0 0 0 5.7 0l2.2-2.2a4 4 0 0 0-5.7-5.7l-1.2 1.2' }),
|
|
867
|
+
h('path', { d: 'M14 10.5a4 4 0 0 0-5.7 0l-2.2 2.2a4 4 0 0 0 5.7 5.7l1.2-1.2' }),
|
|
868
|
+
);
|
|
869
|
+
case 'trash':
|
|
870
|
+
return h(
|
|
871
|
+
'svg',
|
|
872
|
+
common,
|
|
873
|
+
h('path', { d: 'M4 7h16' }),
|
|
874
|
+
h('path', { d: 'M9 7V4h6v3' }),
|
|
875
|
+
h('path', { d: 'M6 7l1 13h10l1-13' }),
|
|
876
|
+
h('path', { d: 'M10 11v6M14 11v6' }),
|
|
877
|
+
);
|
|
878
|
+
case 'more':
|
|
879
|
+
return h(
|
|
880
|
+
'svg',
|
|
881
|
+
common,
|
|
882
|
+
h('circle', { cx: 5.5, cy: 12, r: 1.2, fill: 'currentColor', stroke: 'none' }),
|
|
883
|
+
h('circle', { cx: 12, cy: 12, r: 1.2, fill: 'currentColor', stroke: 'none' }),
|
|
884
|
+
h('circle', { cx: 18.5, cy: 12, r: 1.2, fill: 'currentColor', stroke: 'none' }),
|
|
885
|
+
);
|
|
886
|
+
case 'grip':
|
|
887
|
+
return h(
|
|
888
|
+
'svg',
|
|
889
|
+
common,
|
|
890
|
+
...[
|
|
891
|
+
[9, 6], [15, 6], [9, 12], [15, 12], [9, 18], [15, 18],
|
|
892
|
+
].map(([cx, cy], index) => h('circle', { key: index, cx, cy, r: 1.05, fill: 'currentColor', stroke: 'none' })),
|
|
893
|
+
);
|
|
894
|
+
case 'move':
|
|
895
|
+
return h(
|
|
896
|
+
'svg',
|
|
897
|
+
common,
|
|
898
|
+
h('path', { d: 'M12 3v18M3 12h18' }),
|
|
899
|
+
h('path', { d: 'M12 3l-2.5 2.5M12 3l2.5 2.5M12 21l-2.5-2.5M12 21l2.5-2.5M3 12l2.5-2.5M3 12l2.5 2.5M21 12l-2.5-2.5M21 12l-2.5 2.5' }),
|
|
900
|
+
);
|
|
901
|
+
case 'plus':
|
|
902
|
+
return h('svg', common, h('path', { d: 'M12 5v14M5 12h14' }));
|
|
903
|
+
case 'refresh':
|
|
904
|
+
return h('svg', common, h('path', { d: 'M20 11a8 8 0 1 0-2.3 5.7' }), h('path', { d: 'M20 5v6h-6' }));
|
|
905
|
+
case 'close':
|
|
906
|
+
return h('svg', common, h('path', { d: 'M6 6l12 12M18 6L6 18' }));
|
|
907
|
+
case 'home':
|
|
908
|
+
return h('svg', common, h('path', { d: 'M4 11l8-7 8 7' }), h('path', { d: 'M6 10v10h12V10' }));
|
|
909
|
+
case 'chevronLeft':
|
|
910
|
+
return h('svg', common, h('path', { d: 'M15 5l-7 7 7 7' }));
|
|
911
|
+
case 'chevronRight':
|
|
912
|
+
return h('svg', common, h('path', { d: 'M9 5l7 7-7 7' }));
|
|
913
|
+
case 'chevronDown':
|
|
914
|
+
return h('svg', common, h('path', { d: 'M5 9l7 7 7-7' }));
|
|
915
|
+
case 'restore':
|
|
916
|
+
return h('svg', common, h('path', { d: 'M4 10a8 8 0 1 1 2.3 6.6' }), h('path', { d: 'M4 4v6h6' }));
|
|
917
|
+
case 'check':
|
|
918
|
+
return h('svg', common, h('path', { d: 'M5 13l4 4L19 7' }));
|
|
919
|
+
case 'warning':
|
|
920
|
+
return h('svg', common, h('path', { d: 'M12 3l9 17H3z' }), h('path', { d: 'M12 9v5M12 17.5v.01' }));
|
|
921
|
+
case 'settings':
|
|
922
|
+
return h(
|
|
923
|
+
'svg',
|
|
924
|
+
common,
|
|
925
|
+
h('circle', { cx: 12, cy: 12, r: 3 }),
|
|
926
|
+
h('path', { d: 'M12 3.5v2M12 18.5v2M3.5 12h2M18.5 12h2M6 6l1.4 1.4M16.6 16.6L18 18M18 6l-1.4 1.4M7.4 16.6L6 18' }),
|
|
927
|
+
);
|
|
928
|
+
case 'panel':
|
|
929
|
+
return h(
|
|
930
|
+
'svg',
|
|
931
|
+
common,
|
|
932
|
+
h('rect', { x: 3, y: 4.5, width: 18, height: 15, rx: 2.2 }),
|
|
933
|
+
h('path', { d: 'M14.5 4.5v15' }),
|
|
934
|
+
);
|
|
935
|
+
case 'spark':
|
|
936
|
+
return h('svg', common, h('path', { d: 'M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9L12 17.5l-1.9-5.1L5 10.5l5.1-1.9z' }));
|
|
937
|
+
default:
|
|
938
|
+
return h('svg', common, h('circle', { cx: 12, cy: 12, r: 8 }));
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** The guide's fallback artwork: a line-art folder with a spark. */
|
|
943
|
+
function FilesGuideArtwork({ size = 26 }) {
|
|
944
|
+
return h(
|
|
945
|
+
'svg',
|
|
946
|
+
{
|
|
947
|
+
width: size,
|
|
948
|
+
height: size,
|
|
949
|
+
viewBox: '0 0 24 24',
|
|
950
|
+
fill: 'none',
|
|
951
|
+
stroke: 'currentColor',
|
|
952
|
+
strokeWidth: 1.6,
|
|
953
|
+
strokeLinecap: 'round',
|
|
954
|
+
strokeLinejoin: 'round',
|
|
955
|
+
'aria-hidden': true,
|
|
956
|
+
},
|
|
957
|
+
h('path', { d: 'M3 6.8A2.3 2.3 0 0 1 5.3 4.5h3l2 2h8.4A2.3 2.3 0 0 1 21 8.8v8.4a2.3 2.3 0 0 1-2.3 2.3H5.3A2.3 2.3 0 0 1 3 17.2z' }),
|
|
958
|
+
h('path', { d: 'M15.2 9.6l.75 2.05 2.05.75-2.05.75-.75 2.05-.75-2.05L12.4 12.4l2.05-.75z' }),
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// ------------------------------------------------------------------
|
|
963
|
+
// Small shared UI
|
|
964
|
+
// ------------------------------------------------------------------
|
|
965
|
+
|
|
966
|
+
function MiniMenu(props) {
|
|
967
|
+
const { x, y, items, onClose } = props;
|
|
968
|
+
const width = 224;
|
|
969
|
+
const rows = items.filter((item) => item.separator !== true).length;
|
|
970
|
+
const separators = items.length - rows;
|
|
971
|
+
const height = 10 + rows * 29 + separators * 9;
|
|
972
|
+
const left = Math.max(8, Math.min(x, (window.innerWidth || 1024) - width - 8));
|
|
973
|
+
const top = Math.max(8, Math.min(y, (window.innerHeight || 768) - height - 8));
|
|
974
|
+
return h(
|
|
975
|
+
React.Fragment,
|
|
976
|
+
null,
|
|
977
|
+
h('div', { className: 'dsh-fk-scrim', onClick: onClose, onContextMenu: (event) => { event.preventDefault(); onClose(); } }),
|
|
978
|
+
h(
|
|
979
|
+
'div',
|
|
980
|
+
{ className: 'dsh-fk-menu', role: 'menu', style: { left, top, width } },
|
|
981
|
+
items.map((item, index) => {
|
|
982
|
+
if (item.separator === true) return h('div', { key: `sep-${index}`, className: 'dsh-fk-menusep', role: 'separator' });
|
|
983
|
+
return h(
|
|
984
|
+
'button',
|
|
985
|
+
{
|
|
986
|
+
key: `${index}-${item.label}`,
|
|
987
|
+
type: 'button',
|
|
988
|
+
role: 'menuitem',
|
|
989
|
+
'data-danger': item.danger === true,
|
|
990
|
+
disabled: item.disabled === true,
|
|
991
|
+
onClick: () => {
|
|
992
|
+
onClose();
|
|
993
|
+
item.onSelect();
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
item.icon !== undefined ? h(Icon, { name: item.icon, size: 14 }) : null,
|
|
997
|
+
h('span', { className: 'dsh-fk-menulabel' }, item.label),
|
|
998
|
+
);
|
|
999
|
+
}),
|
|
1000
|
+
),
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function ToolButton({ icon, label, onClick, disabled = false, primary = false }) {
|
|
1005
|
+
return h(
|
|
1006
|
+
'button',
|
|
1007
|
+
{
|
|
1008
|
+
type: 'button',
|
|
1009
|
+
className: primary === true ? 'dsh-fk-btn dsh-fk-btn-primary' : 'dsh-fk-btn',
|
|
1010
|
+
title: label,
|
|
1011
|
+
'aria-label': label,
|
|
1012
|
+
disabled,
|
|
1013
|
+
onClick,
|
|
1014
|
+
},
|
|
1015
|
+
icon !== undefined ? h(Icon, { name: icon, size: 14 }) : null,
|
|
1016
|
+
h('span', null, label),
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function IconButton({ icon, label, onClick, danger = false, disabled = false, onContextMenu }) {
|
|
1021
|
+
return h(
|
|
1022
|
+
'button',
|
|
1023
|
+
{
|
|
1024
|
+
type: 'button',
|
|
1025
|
+
className: 'dsh-fk-iconbtn',
|
|
1026
|
+
title: label,
|
|
1027
|
+
'aria-label': label,
|
|
1028
|
+
'data-danger': danger === true,
|
|
1029
|
+
disabled,
|
|
1030
|
+
onClick,
|
|
1031
|
+
onContextMenu,
|
|
1032
|
+
},
|
|
1033
|
+
h(Icon, { name: icon, size: 15 }),
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function Dialog({ title, children, footer, onClose, wide = false }) {
|
|
1038
|
+
return h(
|
|
1039
|
+
'div',
|
|
1040
|
+
{
|
|
1041
|
+
className: 'dsh-fk-dialogwrap',
|
|
1042
|
+
onMouseDown: (event) => {
|
|
1043
|
+
if (event.target === event.currentTarget) onClose();
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
h(
|
|
1047
|
+
'div',
|
|
1048
|
+
{ className: wide === true ? 'dsh-fk-dialog dsh-fk-dialog-wide' : 'dsh-fk-dialog', role: 'dialog', 'aria-modal': true, 'aria-label': title },
|
|
1049
|
+
h('div', { className: 'dsh-fk-dialogtitle' }, title),
|
|
1050
|
+
h('div', { className: 'dsh-fk-dialogbody' }, children),
|
|
1051
|
+
h('div', { className: 'dsh-fk-dialogfoot' }, footer),
|
|
1052
|
+
),
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function TextDialog({ title, label, initial = '', placeholder = '', confirmText, onSubmit, onClose }) {
|
|
1057
|
+
const [value, setValue] = React.useState(initial);
|
|
1058
|
+
const [busy, setBusy] = React.useState(false);
|
|
1059
|
+
const [error, setError] = React.useState('');
|
|
1060
|
+
const inputRef = React.useRef(null);
|
|
1061
|
+
React.useEffect(() => {
|
|
1062
|
+
const timer = setTimeout(() => inputRef.current?.focus?.(), 30);
|
|
1063
|
+
return () => clearTimeout(timer);
|
|
1064
|
+
}, []);
|
|
1065
|
+
const submit = async (event) => {
|
|
1066
|
+
event?.preventDefault?.();
|
|
1067
|
+
if (busy) return;
|
|
1068
|
+
const name = value.trim();
|
|
1069
|
+
if (name === '') {
|
|
1070
|
+
setError(t('error.bad-name'));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
setBusy(true);
|
|
1074
|
+
setError('');
|
|
1075
|
+
try {
|
|
1076
|
+
const ok = await onSubmit(name);
|
|
1077
|
+
if (ok === true) onClose();
|
|
1078
|
+
} catch (failure) {
|
|
1079
|
+
setError(errorText(failure));
|
|
1080
|
+
} finally {
|
|
1081
|
+
setBusy(false);
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
return h(
|
|
1085
|
+
Dialog,
|
|
1086
|
+
{
|
|
1087
|
+
title,
|
|
1088
|
+
onClose,
|
|
1089
|
+
footer: [
|
|
1090
|
+
h('button', { key: 'cancel', type: 'button', className: 'dsh-fk-btn', onClick: onClose }, t('cancel')),
|
|
1091
|
+
h(
|
|
1092
|
+
'button',
|
|
1093
|
+
{ key: 'go', type: 'submit', form: 'dsh-fk-text-form', className: 'dsh-fk-btn dsh-fk-btn-primary', disabled: busy },
|
|
1094
|
+
busy ? t('saving') : confirmText,
|
|
1095
|
+
),
|
|
1096
|
+
],
|
|
1097
|
+
},
|
|
1098
|
+
h(
|
|
1099
|
+
'form',
|
|
1100
|
+
{ id: 'dsh-fk-text-form', className: 'dsh-fk-form', onSubmit: submit },
|
|
1101
|
+
h('label', { className: 'dsh-fk-label' }, label),
|
|
1102
|
+
h('input', {
|
|
1103
|
+
ref: inputRef,
|
|
1104
|
+
className: 'dsh-fk-input',
|
|
1105
|
+
value,
|
|
1106
|
+
placeholder,
|
|
1107
|
+
spellCheck: false,
|
|
1108
|
+
onChange: (event) => setValue(event.target.value),
|
|
1109
|
+
onKeyDown: (event) => {
|
|
1110
|
+
if (event.key === 'Enter') submit(event);
|
|
1111
|
+
},
|
|
1112
|
+
}),
|
|
1113
|
+
error !== '' ? h('p', { className: 'dsh-fk-error' }, error) : null,
|
|
1114
|
+
),
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function ConfirmDialog({ title, text, hint, danger = false, confirmText, onConfirm, onClose }) {
|
|
1119
|
+
const [busy, setBusy] = React.useState(false);
|
|
1120
|
+
const [error, setError] = React.useState('');
|
|
1121
|
+
const run = async () => {
|
|
1122
|
+
if (busy) return;
|
|
1123
|
+
setBusy(true);
|
|
1124
|
+
setError('');
|
|
1125
|
+
try {
|
|
1126
|
+
const ok = await onConfirm();
|
|
1127
|
+
if (ok === true) onClose();
|
|
1128
|
+
} catch (failure) {
|
|
1129
|
+
setError(errorText(failure));
|
|
1130
|
+
} finally {
|
|
1131
|
+
setBusy(false);
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
return h(
|
|
1135
|
+
Dialog,
|
|
1136
|
+
{
|
|
1137
|
+
title,
|
|
1138
|
+
onClose,
|
|
1139
|
+
footer: [
|
|
1140
|
+
h('button', { key: 'cancel', type: 'button', className: 'dsh-fk-btn', onClick: onClose }, t('cancel')),
|
|
1141
|
+
h(
|
|
1142
|
+
'button',
|
|
1143
|
+
{
|
|
1144
|
+
key: 'go',
|
|
1145
|
+
type: 'button',
|
|
1146
|
+
className: danger === true ? 'dsh-fk-btn dsh-fk-btn-danger' : 'dsh-fk-btn dsh-fk-btn-primary',
|
|
1147
|
+
disabled: busy,
|
|
1148
|
+
onClick: run,
|
|
1149
|
+
},
|
|
1150
|
+
busy ? t('saving') : confirmText,
|
|
1151
|
+
),
|
|
1152
|
+
],
|
|
1153
|
+
},
|
|
1154
|
+
h('p', { className: 'dsh-fk-dialogtext' }, text),
|
|
1155
|
+
hint !== undefined ? h('p', { className: 'dsh-fk-hint' }, hint) : null,
|
|
1156
|
+
error !== '' ? h('p', { className: 'dsh-fk-error' }, error) : null,
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// ------------------------------------------------------------------
|
|
1161
|
+
// Move dialog
|
|
1162
|
+
// ------------------------------------------------------------------
|
|
1163
|
+
|
|
1164
|
+
function MoveDialog({ root, rel, sourceIsDir = false, onMove, onClose }) {
|
|
1165
|
+
const sourceDir = dirOf(rel);
|
|
1166
|
+
const [cwd, setCwd] = React.useState(sourceDir);
|
|
1167
|
+
const [dirs, setDirs] = React.useState([]);
|
|
1168
|
+
const [loading, setLoading] = React.useState(true);
|
|
1169
|
+
const [error, setError] = React.useState('');
|
|
1170
|
+
const [busy, setBusy] = React.useState(false);
|
|
1171
|
+
|
|
1172
|
+
React.useEffect(() => {
|
|
1173
|
+
let dead = false;
|
|
1174
|
+
setLoading(true);
|
|
1175
|
+
setError('');
|
|
1176
|
+
api('list', { root, path: cwd })
|
|
1177
|
+
.then((data) => {
|
|
1178
|
+
if (dead) return;
|
|
1179
|
+
setDirs(data.entries.filter((entry) => entry.kind === 'directory' && !isHiddenEntry(entry.name)));
|
|
1180
|
+
setLoading(false);
|
|
1181
|
+
})
|
|
1182
|
+
.catch((failure) => {
|
|
1183
|
+
if (dead) return;
|
|
1184
|
+
setError(errorText(failure));
|
|
1185
|
+
setLoading(false);
|
|
1186
|
+
});
|
|
1187
|
+
return () => {
|
|
1188
|
+
dead = true;
|
|
1189
|
+
};
|
|
1190
|
+
}, [root, cwd]);
|
|
1191
|
+
|
|
1192
|
+
const invalidTarget = cwd === sourceDir || (sourceIsDir && (cwd === rel || cwd.startsWith(rel + '/')));
|
|
1193
|
+
|
|
1194
|
+
const move = async () => {
|
|
1195
|
+
if (busy || invalidTarget) return;
|
|
1196
|
+
setBusy(true);
|
|
1197
|
+
setError('');
|
|
1198
|
+
try {
|
|
1199
|
+
const ok = await onMove(cwd);
|
|
1200
|
+
if (ok === true) onClose();
|
|
1201
|
+
} catch (failure) {
|
|
1202
|
+
setError(errorText(failure));
|
|
1203
|
+
} finally {
|
|
1204
|
+
setBusy(false);
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const parts = cwd === '' ? [] : cwd.split('/');
|
|
1209
|
+
return h(
|
|
1210
|
+
Dialog,
|
|
1211
|
+
{
|
|
1212
|
+
title: t('moveTo'),
|
|
1213
|
+
onClose,
|
|
1214
|
+
wide: true,
|
|
1215
|
+
footer: [
|
|
1216
|
+
h('button', { key: 'cancel', type: 'button', className: 'dsh-fk-btn', onClick: onClose }, t('cancel')),
|
|
1217
|
+
h(
|
|
1218
|
+
'button',
|
|
1219
|
+
{
|
|
1220
|
+
key: 'go',
|
|
1221
|
+
type: 'button',
|
|
1222
|
+
className: 'dsh-fk-btn dsh-fk-btn-primary',
|
|
1223
|
+
disabled: busy || invalidTarget,
|
|
1224
|
+
onClick: move,
|
|
1225
|
+
},
|
|
1226
|
+
busy ? t('saving') : t('moveHere'),
|
|
1227
|
+
),
|
|
1228
|
+
],
|
|
1229
|
+
},
|
|
1230
|
+
h(
|
|
1231
|
+
'div',
|
|
1232
|
+
{ className: 'dsh-fk-crumbs dsh-fk-dialog-crumbs' },
|
|
1233
|
+
h('button', { type: 'button', className: 'dsh-fk-crumb', title: root, onClick: () => setCwd('') }, h(Icon, { name: 'home', size: 13 }), workspaceName(root)),
|
|
1234
|
+
parts.map((segment, index) =>
|
|
1235
|
+
h(
|
|
1236
|
+
React.Fragment,
|
|
1237
|
+
{ key: `${index}-${segment}` },
|
|
1238
|
+
h(Icon, { name: 'chevronLeft', size: 11 }),
|
|
1239
|
+
h('button', { type: 'button', className: 'dsh-fk-crumb', onClick: () => setCwd(parts.slice(0, index + 1).join('/')) }, segment),
|
|
1240
|
+
),
|
|
1241
|
+
),
|
|
1242
|
+
),
|
|
1243
|
+
h('p', { className: 'dsh-fk-hint' }, t('chooseDir')),
|
|
1244
|
+
h(
|
|
1245
|
+
'div',
|
|
1246
|
+
{ className: 'dsh-fk-picklist' },
|
|
1247
|
+
cwd !== ''
|
|
1248
|
+
? h(
|
|
1249
|
+
'button',
|
|
1250
|
+
{ type: 'button', className: 'dsh-fk-pickrow', onClick: () => setCwd(dirOf(cwd)) },
|
|
1251
|
+
h(Icon, { name: 'chevronLeft', size: 14 }),
|
|
1252
|
+
h('span', null, t('parentDir')),
|
|
1253
|
+
)
|
|
1254
|
+
: null,
|
|
1255
|
+
loading ? h('p', { className: 'dsh-fk-hint' }, t('loading')) : null,
|
|
1256
|
+
!loading && dirs.length === 0 ? h('p', { className: 'dsh-fk-hint' }, t('noSubdirs')) : null,
|
|
1257
|
+
dirs.map((entry) =>
|
|
1258
|
+
h(
|
|
1259
|
+
'button',
|
|
1260
|
+
{ key: entry.path, type: 'button', className: 'dsh-fk-pickrow', onClick: () => setCwd(entry.path) },
|
|
1261
|
+
h(Icon, { name: 'folder', size: 14 }),
|
|
1262
|
+
h('span', null, entry.name),
|
|
1263
|
+
),
|
|
1264
|
+
),
|
|
1265
|
+
),
|
|
1266
|
+
error !== '' ? h('p', { className: 'dsh-fk-error' }, error) : null,
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ------------------------------------------------------------------
|
|
1271
|
+
// Drag helpers
|
|
1272
|
+
// ------------------------------------------------------------------
|
|
1273
|
+
|
|
1274
|
+
function canDropInto(source, toDir) {
|
|
1275
|
+
if (source === null) return false;
|
|
1276
|
+
if (toDir === source.rel) return false;
|
|
1277
|
+
if (source.kind === 'directory' && toDir.startsWith(source.rel + '/')) return false;
|
|
1278
|
+
return dirOf(source.rel) !== toDir;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// ------------------------------------------------------------------
|
|
1282
|
+
// FileKit manager (the `filekit` page)
|
|
1283
|
+
// ------------------------------------------------------------------
|
|
1284
|
+
|
|
1285
|
+
function FileKitTab(props) {
|
|
1286
|
+
const { useTabInfo, sessionId, inputActions } = props;
|
|
1287
|
+
const { tab } = useTabInfo();
|
|
1288
|
+
const [root, setRoot] = React.useState('');
|
|
1289
|
+
const [rootError, setRootError] = React.useState('');
|
|
1290
|
+
const [attempt, setAttempt] = React.useState(0);
|
|
1291
|
+
|
|
1292
|
+
React.useEffect(() => {
|
|
1293
|
+
if (typeof sessionId !== 'string' || sessionId === '') return undefined;
|
|
1294
|
+
let dead = false;
|
|
1295
|
+
setRoot('');
|
|
1296
|
+
setRootError('');
|
|
1297
|
+
api('workspace', { sessionId })
|
|
1298
|
+
.then((data) => {
|
|
1299
|
+
if (!dead) setRoot(data.root);
|
|
1300
|
+
})
|
|
1301
|
+
.catch((failure) => {
|
|
1302
|
+
if (!dead) setRootError(errorText(failure));
|
|
1303
|
+
});
|
|
1304
|
+
return () => {
|
|
1305
|
+
dead = true;
|
|
1306
|
+
};
|
|
1307
|
+
}, [sessionId, attempt]);
|
|
1308
|
+
|
|
1309
|
+
if (root === '' && rootError === '') {
|
|
1310
|
+
return h('div', { className: 'dsh-fk-tabstatus' }, h('p', { className: 'dsh-fk-hint' }, t('loading')));
|
|
1311
|
+
}
|
|
1312
|
+
if (root === '') {
|
|
1313
|
+
return h(
|
|
1314
|
+
'div',
|
|
1315
|
+
{ className: 'dsh-fk-tabstatus' },
|
|
1316
|
+
h('p', { className: 'dsh-fk-error' }, rootError),
|
|
1317
|
+
h('button', { type: 'button', className: 'dsh-fk-btn', onClick: () => setAttempt((value) => value + 1) }, t('retry')),
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
return h(FileKitManager, { root, sessionId, inputActions, tab });
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function FileKitManager({ root, sessionId, inputActions, tab }) {
|
|
1324
|
+
const [cwd, setCwd] = React.useState('');
|
|
1325
|
+
const [entries, setEntries] = React.useState([]);
|
|
1326
|
+
const [loading, setLoading] = React.useState(false);
|
|
1327
|
+
const [listError, setListError] = React.useState('');
|
|
1328
|
+
const [reload, setReload] = React.useState(0);
|
|
1329
|
+
const [view, setView] = React.useState('browse');
|
|
1330
|
+
const [trashEntries, setTrashEntries] = React.useState([]);
|
|
1331
|
+
const [trashLoading, setTrashLoading] = React.useState(false);
|
|
1332
|
+
const [editor, setEditor] = React.useState(null);
|
|
1333
|
+
const [draft, setDraft] = React.useState('');
|
|
1334
|
+
const [saving, setSaving] = React.useState(false);
|
|
1335
|
+
const [dialog, setDialog] = React.useState(null);
|
|
1336
|
+
const [selected, setSelected] = React.useState(null);
|
|
1337
|
+
const [menu, setMenu] = React.useState(null);
|
|
1338
|
+
const [notice, setNotice] = React.useState(null);
|
|
1339
|
+
const [dragging, setDragging] = React.useState(null);
|
|
1340
|
+
const [dropDir, setDropDir] = React.useState(null);
|
|
1341
|
+
const settingsSnapshot = useSettings();
|
|
1342
|
+
const clipboard = useFileClipboard();
|
|
1343
|
+
const canPaste = clipboard !== null && clipboard.root === root;
|
|
1344
|
+
const navigation = tab.navigation;
|
|
1345
|
+
const editorRef = React.useRef(null);
|
|
1346
|
+
const draftRef = React.useRef('');
|
|
1347
|
+
const savingRef = React.useRef(false);
|
|
1348
|
+
const saveRef = React.useRef(null);
|
|
1349
|
+
const draggingRef = React.useRef(null);
|
|
1350
|
+
const appliedRevisionRef = React.useRef(0);
|
|
1351
|
+
editorRef.current = editor;
|
|
1352
|
+
draftRef.current = draft;
|
|
1353
|
+
|
|
1354
|
+
const dirty = editor !== null && draft !== editor.original;
|
|
1355
|
+
|
|
1356
|
+
React.useEffect(() => {
|
|
1357
|
+
lastTabActions = tab.actions;
|
|
1358
|
+
}, [tab.actions]);
|
|
1359
|
+
|
|
1360
|
+
React.useEffect(() => {
|
|
1361
|
+
setTabDirty(tab.id, dirty);
|
|
1362
|
+
return () => setTabDirty(tab.id, false);
|
|
1363
|
+
}, [tab.id, dirty]);
|
|
1364
|
+
|
|
1365
|
+
React.useEffect(() => {
|
|
1366
|
+
if (notice === null) return undefined;
|
|
1367
|
+
const timer = setTimeout(() => setNotice(null), 5000);
|
|
1368
|
+
return () => clearTimeout(timer);
|
|
1369
|
+
}, [notice]);
|
|
1370
|
+
|
|
1371
|
+
const loadEditor = React.useCallback(
|
|
1372
|
+
async (rel) => {
|
|
1373
|
+
if (root === '') return;
|
|
1374
|
+
try {
|
|
1375
|
+
const data = await api('read', { root, path: rel, sessionId });
|
|
1376
|
+
if (data.binary === true) {
|
|
1377
|
+
setNotice({ kind: 'error', text: t('binary') });
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
setEditor({
|
|
1381
|
+
rel,
|
|
1382
|
+
original: data.text,
|
|
1383
|
+
mtimeMs: data.mtimeMs,
|
|
1384
|
+
size: data.size,
|
|
1385
|
+
truncated: data.truncated === true,
|
|
1386
|
+
});
|
|
1387
|
+
setDraft(data.text);
|
|
1388
|
+
setMenu(null);
|
|
1389
|
+
setDialog(null);
|
|
1390
|
+
setNotice(null);
|
|
1391
|
+
} catch (failure) {
|
|
1392
|
+
setNotice({ kind: 'error', text: errorText(failure) });
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
[root, sessionId],
|
|
1396
|
+
);
|
|
1397
|
+
|
|
1398
|
+
const refresh = React.useCallback(() => setReload((value) => value + 1), []);
|
|
1399
|
+
|
|
1400
|
+
React.useEffect(() => {
|
|
1401
|
+
if (root === '' || view !== 'browse') return undefined;
|
|
1402
|
+
let dead = false;
|
|
1403
|
+
setLoading(true);
|
|
1404
|
+
setListError('');
|
|
1405
|
+
api('list', { root, path: cwd, sessionId })
|
|
1406
|
+
.then((data) => {
|
|
1407
|
+
if (dead) return;
|
|
1408
|
+
setEntries(data.entries);
|
|
1409
|
+
setLoading(false);
|
|
1410
|
+
})
|
|
1411
|
+
.catch((failure) => {
|
|
1412
|
+
if (dead) return;
|
|
1413
|
+
setListError(errorText(failure));
|
|
1414
|
+
setLoading(false);
|
|
1415
|
+
});
|
|
1416
|
+
return () => {
|
|
1417
|
+
dead = true;
|
|
1418
|
+
};
|
|
1419
|
+
}, [root, cwd, view, reload, sessionId]);
|
|
1420
|
+
|
|
1421
|
+
React.useEffect(() => {
|
|
1422
|
+
if (root === '' || view !== 'trash') return undefined;
|
|
1423
|
+
let dead = false;
|
|
1424
|
+
setTrashLoading(true);
|
|
1425
|
+
api('trashList', { root, sessionId })
|
|
1426
|
+
.then((data) => {
|
|
1427
|
+
if (dead) return;
|
|
1428
|
+
setTrashEntries(data.entries);
|
|
1429
|
+
setTrashLoading(false);
|
|
1430
|
+
})
|
|
1431
|
+
.catch((failure) => {
|
|
1432
|
+
if (dead) return;
|
|
1433
|
+
setNotice({ kind: 'error', text: errorText(failure) });
|
|
1434
|
+
setTrashLoading(false);
|
|
1435
|
+
});
|
|
1436
|
+
return () => {
|
|
1437
|
+
dead = true;
|
|
1438
|
+
};
|
|
1439
|
+
}, [root, view, reload, sessionId]);
|
|
1440
|
+
|
|
1441
|
+
// Auto refresh the current folder.
|
|
1442
|
+
React.useEffect(() => {
|
|
1443
|
+
if (root === '' || view !== 'browse' || settingsSnapshot.autoRefreshSeconds <= 0) return undefined;
|
|
1444
|
+
const timer = setInterval(() => {
|
|
1445
|
+
if (document.visibilityState === 'hidden' || editorRef.current !== null) return;
|
|
1446
|
+
refresh();
|
|
1447
|
+
}, settingsSnapshot.autoRefreshSeconds * 1000);
|
|
1448
|
+
return () => clearInterval(timer);
|
|
1449
|
+
}, [root, view, settingsSnapshot.autoRefreshSeconds, refresh]);
|
|
1450
|
+
|
|
1451
|
+
// Page-refresh shortcut.
|
|
1452
|
+
React.useEffect(() => {
|
|
1453
|
+
if (typeof tab.actions?.bindCommands !== 'function') return undefined;
|
|
1454
|
+
return tab.actions.bindCommands({ refresh: () => refresh() });
|
|
1455
|
+
}, [tab.actions, refresh]);
|
|
1456
|
+
|
|
1457
|
+
// Apply navigation params (open editor, trash view, folder).
|
|
1458
|
+
React.useEffect(() => {
|
|
1459
|
+
if (root === '' || navigation === undefined) return;
|
|
1460
|
+
const revision = navigation.revision ?? 0;
|
|
1461
|
+
if (revision === appliedRevisionRef.current) return;
|
|
1462
|
+
appliedRevisionRef.current = revision;
|
|
1463
|
+
const params = navigation.params;
|
|
1464
|
+
if (params === undefined || params === null) return;
|
|
1465
|
+
if (dirty && typeof params.rel === 'string' && params.rel !== '') {
|
|
1466
|
+
showToast(t('saveBeforeSwitch'), 'error');
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
if (params.view === 'trash') {
|
|
1470
|
+
setView('trash');
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
setView('browse');
|
|
1474
|
+
if (typeof params.dir === 'string') {
|
|
1475
|
+
setCwd(params.dir);
|
|
1476
|
+
} else if (typeof params.rel === 'string' && params.rel !== '') {
|
|
1477
|
+
setCwd(dirOf(params.rel));
|
|
1478
|
+
}
|
|
1479
|
+
if (typeof params.rel !== 'string' || params.rel === '') return;
|
|
1480
|
+
const action = params.action;
|
|
1481
|
+
if (action === 'edit') loadEditor(params.rel);
|
|
1482
|
+
else if (action === 'rename') setDialog({ kind: 'rename', rel: params.rel });
|
|
1483
|
+
else if (action === 'move') setDialog({ kind: 'move', rel: params.rel });
|
|
1484
|
+
else if (action === 'delete') setDialog({ kind: 'delete', rel: params.rel });
|
|
1485
|
+
else setSelected(params.rel);
|
|
1486
|
+
}, [root, navigation, dirty, loadEditor]);
|
|
1487
|
+
|
|
1488
|
+
const succeed = (text) => {
|
|
1489
|
+
setNotice({ kind: 'ok', text });
|
|
1490
|
+
refresh();
|
|
1491
|
+
setMenu(null);
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
const mutate = async (action, successText) => {
|
|
1495
|
+
if (root === '') return false;
|
|
1496
|
+
try {
|
|
1497
|
+
const result = await action();
|
|
1498
|
+
succeed(typeof successText === 'function' ? successText(result) : successText);
|
|
1499
|
+
return true;
|
|
1500
|
+
} catch (failure) {
|
|
1501
|
+
setNotice({ kind: 'error', text: errorText(failure) });
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
/** Paste the FileKit clipboard into one directory; a taken name gets " (n)". */
|
|
1507
|
+
const pasteInto = async (toDir) => {
|
|
1508
|
+
const clip = fileClipboard;
|
|
1509
|
+
if (clip === null || clip.root !== root) return false;
|
|
1510
|
+
const ok = await mutate(
|
|
1511
|
+
() => api(clip.mode === 'cut' ? 'move' : 'copy', { root, path: clip.rel, toDir, unique: true, sessionId }),
|
|
1512
|
+
(result) => t('pastedTo', { path: result.path }),
|
|
1513
|
+
);
|
|
1514
|
+
if (ok && clip.mode === 'cut') setFileClipboard(null);
|
|
1515
|
+
return ok;
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
const copyRelPath = async (rel) => {
|
|
1519
|
+
const ok = await copyText(rel);
|
|
1520
|
+
showToast(ok ? t('copiedPath') : t('error.internal'), ok ? 'ok' : 'error');
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
const copyAbsPath = async (rel) => {
|
|
1524
|
+
const ok = await copyText(joinAbs(root, rel));
|
|
1525
|
+
showToast(ok ? t('copiedPath') : t('error.internal'), ok ? 'ok' : 'error');
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
const referenceRel = async (rel) => {
|
|
1529
|
+
const text = `@${rel} `;
|
|
1530
|
+
if (inputActions !== undefined && typeof inputActions.captureInsertion === 'function' && typeof inputActions.insertText === 'function') {
|
|
1531
|
+
try {
|
|
1532
|
+
const span = inputActions.captureInsertion();
|
|
1533
|
+
if (inputActions.insertText(text, span) === true) {
|
|
1534
|
+
inputActions.persistDraft?.();
|
|
1535
|
+
showToast(t('referenced'));
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
} catch {
|
|
1539
|
+
/* fall back to the clipboard */
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const ok = await copyText(text);
|
|
1543
|
+
showToast(ok ? t('referencedCopied') : t('error.internal'), ok ? 'ok' : 'error');
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
const openPreview = (rel) => {
|
|
1547
|
+
if (typeof tab.actions?.openResource !== 'function') return;
|
|
1548
|
+
try {
|
|
1549
|
+
tab.actions.openResource(fileAddress(sessionId, rel));
|
|
1550
|
+
} catch (failure) {
|
|
1551
|
+
showToast(errorText(failure), 'error');
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
const discardEditor = () => {
|
|
1556
|
+
setEditor(null);
|
|
1557
|
+
setDraft('');
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
const closeEditor = () => {
|
|
1561
|
+
if (dirty) {
|
|
1562
|
+
setDialog({ kind: 'discard', after: 'editor' });
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
discardEditor();
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
const saveEditor = async () => {
|
|
1569
|
+
const current = editorRef.current;
|
|
1570
|
+
const text = draftRef.current;
|
|
1571
|
+
if (current === null || savingRef.current || current.truncated || text === current.original) return;
|
|
1572
|
+
savingRef.current = true;
|
|
1573
|
+
setSaving(true);
|
|
1574
|
+
try {
|
|
1575
|
+
const data = await api('write', {
|
|
1576
|
+
root,
|
|
1577
|
+
path: current.rel,
|
|
1578
|
+
content: text,
|
|
1579
|
+
expectedMtimeMs: current.mtimeMs,
|
|
1580
|
+
sessionId,
|
|
1581
|
+
});
|
|
1582
|
+
setEditor({ ...current, original: text, mtimeMs: data.mtimeMs, size: data.size });
|
|
1583
|
+
setNotice({ kind: 'ok', text: t('saved') });
|
|
1584
|
+
refresh();
|
|
1585
|
+
} catch (failure) {
|
|
1586
|
+
setNotice({ kind: 'error', text: errorText(failure) });
|
|
1587
|
+
} finally {
|
|
1588
|
+
savingRef.current = false;
|
|
1589
|
+
setSaving(false);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
saveRef.current = saveEditor;
|
|
1593
|
+
|
|
1594
|
+
React.useEffect(() => {
|
|
1595
|
+
const onKeyDown = (event) => {
|
|
1596
|
+
if (event.key === 'Escape') {
|
|
1597
|
+
if (menu !== null) {
|
|
1598
|
+
setMenu(null);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (dialog !== null) {
|
|
1602
|
+
setDialog(null);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (editorRef.current !== null) {
|
|
1606
|
+
if (draftRef.current !== editorRef.current.original) setDialog({ kind: 'discard', after: 'editor' });
|
|
1607
|
+
else discardEditor();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's' && editorRef.current !== null) {
|
|
1611
|
+
event.preventDefault();
|
|
1612
|
+
saveRef.current?.();
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
window.addEventListener('keydown', onKeyDown);
|
|
1616
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
1617
|
+
}, [menu, dialog]);
|
|
1618
|
+
|
|
1619
|
+
const nameById = (rel) => baseOf(rel);
|
|
1620
|
+
|
|
1621
|
+
const openRowMenu = (event, entry) => {
|
|
1622
|
+
event.preventDefault();
|
|
1623
|
+
event.stopPropagation();
|
|
1624
|
+
setMenu({ x: event.clientX, y: event.clientY, rel: entry.path, kind: entry.kind, name: entry.name });
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
const openBlankMenu = (event) => {
|
|
1628
|
+
if (typeof event.target.closest === 'function' && event.target.closest('.dsh-fk-row, .dsh-fk-trow') !== null) return;
|
|
1629
|
+
event.preventDefault();
|
|
1630
|
+
setMenu({ x: event.clientX, y: event.clientY, rel: null, kind: 'directory' });
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
const startDrag = (event, entry) => {
|
|
1634
|
+
event.stopPropagation();
|
|
1635
|
+
draggingRef.current = { rel: entry.path, kind: entry.kind };
|
|
1636
|
+
setDragging(draggingRef.current);
|
|
1637
|
+
try {
|
|
1638
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
1639
|
+
event.dataTransfer.setData('text/plain', entry.path);
|
|
1640
|
+
} catch {
|
|
1641
|
+
/* some browsers refuse the drag payload; the ref still carries it */
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
const endDrag = () => {
|
|
1646
|
+
draggingRef.current = null;
|
|
1647
|
+
setDragging(null);
|
|
1648
|
+
setDropDir(null);
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
const moveRow = async (rel, kind, toDir) => {
|
|
1652
|
+
if (!canDropInto({ rel, kind }, toDir)) {
|
|
1653
|
+
setDropDir(null);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
setDropDir(null);
|
|
1657
|
+
await mutate(() => api('move', { root, path: rel, toDir, sessionId }), t('moved'));
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
// ----- dialogs ---------------------------------------------------
|
|
1661
|
+
const dialogNodes = [];
|
|
1662
|
+
if (dialog !== null) {
|
|
1663
|
+
if (dialog.kind === 'create-file' || dialog.kind === 'create-dir') {
|
|
1664
|
+
const isDir = dialog.kind === 'create-dir';
|
|
1665
|
+
const targetDir = typeof dialog.dir === 'string' ? dialog.dir : cwd;
|
|
1666
|
+
dialogNodes.push(
|
|
1667
|
+
h(TextDialog, {
|
|
1668
|
+
key: isDir ? 'create-dir' : 'create-file',
|
|
1669
|
+
title: isDir ? t('newDir') : t('newFile'),
|
|
1670
|
+
label: t('name'),
|
|
1671
|
+
initial: '',
|
|
1672
|
+
placeholder: isDir ? 'assets' : 'notes.md',
|
|
1673
|
+
confirmText: t('create'),
|
|
1674
|
+
onClose: () => setDialog(null),
|
|
1675
|
+
onSubmit: (name) =>
|
|
1676
|
+
mutate(
|
|
1677
|
+
() => api('create', { root, path: joinRel(targetDir, name), kind: isDir ? 'directory' : 'file', sessionId }),
|
|
1678
|
+
t('created'),
|
|
1679
|
+
),
|
|
1680
|
+
}),
|
|
1681
|
+
);
|
|
1682
|
+
} else if (dialog.kind === 'rename') {
|
|
1683
|
+
dialogNodes.push(
|
|
1684
|
+
h(TextDialog, {
|
|
1685
|
+
key: 'rename',
|
|
1686
|
+
title: t('rename'),
|
|
1687
|
+
label: t('newName'),
|
|
1688
|
+
initial: nameById(dialog.rel),
|
|
1689
|
+
confirmText: t('rename'),
|
|
1690
|
+
onClose: () => setDialog(null),
|
|
1691
|
+
onSubmit: (name) => mutate(() => api('rename', { root, path: dialog.rel, name, sessionId }), t('renamed')),
|
|
1692
|
+
}),
|
|
1693
|
+
);
|
|
1694
|
+
} else if (dialog.kind === 'move') {
|
|
1695
|
+
dialogNodes.push(
|
|
1696
|
+
h(MoveDialog, {
|
|
1697
|
+
key: 'move',
|
|
1698
|
+
root,
|
|
1699
|
+
rel: dialog.rel,
|
|
1700
|
+
sourceIsDir: dialog.entryKind === 'directory',
|
|
1701
|
+
onClose: () => setDialog(null),
|
|
1702
|
+
onMove: (toDir) => mutate(() => api('move', { root, path: dialog.rel, toDir, sessionId }), t('moved')),
|
|
1703
|
+
}),
|
|
1704
|
+
);
|
|
1705
|
+
} else if (dialog.kind === 'delete') {
|
|
1706
|
+
dialogNodes.push(
|
|
1707
|
+
h(ConfirmDialog, {
|
|
1708
|
+
key: 'delete',
|
|
1709
|
+
title: t('deleteTitle'),
|
|
1710
|
+
text: dialog.rel,
|
|
1711
|
+
hint: t('deleteHint', { trash: DSH_TRASH }),
|
|
1712
|
+
danger: true,
|
|
1713
|
+
confirmText: t('deleteConfirm'),
|
|
1714
|
+
onClose: () => setDialog(null),
|
|
1715
|
+
onConfirm: async () => {
|
|
1716
|
+
const ok = await mutate(() => api('delete', { root, path: dialog.rel, sessionId }), t('deleted'));
|
|
1717
|
+
if (ok && editor !== null && editor.rel === dialog.rel) discardEditor();
|
|
1718
|
+
return ok;
|
|
1719
|
+
},
|
|
1720
|
+
}),
|
|
1721
|
+
);
|
|
1722
|
+
} else if (dialog.kind === 'purge') {
|
|
1723
|
+
dialogNodes.push(
|
|
1724
|
+
h(ConfirmDialog, {
|
|
1725
|
+
key: 'purge',
|
|
1726
|
+
title: t('purgeTitle'),
|
|
1727
|
+
text: dialog.entry?.name ?? dialog.id,
|
|
1728
|
+
hint: t('purgeHint'),
|
|
1729
|
+
danger: true,
|
|
1730
|
+
confirmText: t('purgeConfirm'),
|
|
1731
|
+
onClose: () => setDialog(null),
|
|
1732
|
+
onConfirm: () => mutate(() => api('purge', { root, id: dialog.id, sessionId }), t('purged')),
|
|
1733
|
+
}),
|
|
1734
|
+
);
|
|
1735
|
+
} else if (dialog.kind === 'empty-trash') {
|
|
1736
|
+
dialogNodes.push(
|
|
1737
|
+
h(ConfirmDialog, {
|
|
1738
|
+
key: 'empty-trash',
|
|
1739
|
+
title: t('emptyTitle'),
|
|
1740
|
+
text: t('emptyHint'),
|
|
1741
|
+
danger: true,
|
|
1742
|
+
confirmText: t('emptyConfirm'),
|
|
1743
|
+
onClose: () => setDialog(null),
|
|
1744
|
+
onConfirm: () => mutate(() => api('emptyTrash', { root, sessionId }), t('emptied')),
|
|
1745
|
+
}),
|
|
1746
|
+
);
|
|
1747
|
+
} else if (dialog.kind === 'discard') {
|
|
1748
|
+
dialogNodes.push(
|
|
1749
|
+
h(ConfirmDialog, {
|
|
1750
|
+
key: 'discard',
|
|
1751
|
+
title: t('discardTitle'),
|
|
1752
|
+
text: t('dirty'),
|
|
1753
|
+
danger: true,
|
|
1754
|
+
confirmText: t('discardConfirm'),
|
|
1755
|
+
onClose: () => setDialog(null),
|
|
1756
|
+
onConfirm: () => {
|
|
1757
|
+
discardEditor();
|
|
1758
|
+
return true;
|
|
1759
|
+
},
|
|
1760
|
+
}),
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// ----- recycle bin view -----------------------------------------
|
|
1766
|
+
const trashView = h(
|
|
1767
|
+
React.Fragment,
|
|
1768
|
+
null,
|
|
1769
|
+
h(
|
|
1770
|
+
'div',
|
|
1771
|
+
{ className: 'dsh-fk-toolbar' },
|
|
1772
|
+
h(
|
|
1773
|
+
'div',
|
|
1774
|
+
{ className: 'dsh-fk-crumbs' },
|
|
1775
|
+
h(
|
|
1776
|
+
'button',
|
|
1777
|
+
{ type: 'button', className: 'dsh-fk-crumb', onClick: () => setView('browse'), title: t('trash') },
|
|
1778
|
+
h(Icon, { name: 'chevronLeft', size: 13 }),
|
|
1779
|
+
t('rootLabel'),
|
|
1780
|
+
),
|
|
1781
|
+
h('span', { className: 'dsh-fk-crumb-sep' }, '/'),
|
|
1782
|
+
h('span', { className: 'dsh-fk-crumb dsh-fk-crumb-current' }, t('trash')),
|
|
1783
|
+
),
|
|
1784
|
+
h(
|
|
1785
|
+
'div',
|
|
1786
|
+
{ className: 'dsh-fk-toolbar-actions' },
|
|
1787
|
+
h(ToolButton, {
|
|
1788
|
+
icon: 'trash',
|
|
1789
|
+
label: t('purge'),
|
|
1790
|
+
disabled: trashEntries.length === 0,
|
|
1791
|
+
onClick: () => setDialog({ kind: 'empty-trash' }),
|
|
1792
|
+
}),
|
|
1793
|
+
h(ToolButton, { icon: 'refresh', label: t('refresh'), onClick: refresh }),
|
|
1794
|
+
),
|
|
1795
|
+
),
|
|
1796
|
+
h(
|
|
1797
|
+
'div',
|
|
1798
|
+
{ className: 'dsh-fk-body' },
|
|
1799
|
+
trashLoading ? h('p', { className: 'dsh-fk-hint dsh-fk-center' }, t('loading')) : null,
|
|
1800
|
+
!trashLoading && trashEntries.length === 0 ? h('p', { className: 'dsh-fk-hint dsh-fk-center' }, t('trashEmpty')) : null,
|
|
1801
|
+
trashEntries.map((entry) =>
|
|
1802
|
+
h(
|
|
1803
|
+
'div',
|
|
1804
|
+
{ key: entry.id, className: 'dsh-fk-row dsh-fk-row-static' },
|
|
1805
|
+
h('span', { className: 'dsh-fk-rowicon' }, h(Icon, { name: 'trash', size: 15 })),
|
|
1806
|
+
h(
|
|
1807
|
+
'span',
|
|
1808
|
+
{ className: 'dsh-fk-rowmain' },
|
|
1809
|
+
h('span', { className: 'dsh-fk-rowname' }, entry.name ?? entry.id),
|
|
1810
|
+
h('span', { className: 'dsh-fk-rowpath', title: entry.originalPath }, entry.originalPath),
|
|
1811
|
+
),
|
|
1812
|
+
h('span', { className: 'dsh-fk-rowmeta' }, entry.deletedAt ? new Date(entry.deletedAt).toLocaleString() : ''),
|
|
1813
|
+
h(IconButton, {
|
|
1814
|
+
icon: 'restore',
|
|
1815
|
+
label: t('restore'),
|
|
1816
|
+
onClick: async () => {
|
|
1817
|
+
const ok = await mutate(() => api('restore', { root, id: entry.id, sessionId }), t('restored'));
|
|
1818
|
+
if (ok) setView('browse');
|
|
1819
|
+
},
|
|
1820
|
+
}),
|
|
1821
|
+
h(IconButton, {
|
|
1822
|
+
icon: 'trash',
|
|
1823
|
+
label: t('purge'),
|
|
1824
|
+
danger: true,
|
|
1825
|
+
onClick: () => setDialog({ kind: 'purge', id: entry.id, entry }),
|
|
1826
|
+
}),
|
|
1827
|
+
),
|
|
1828
|
+
),
|
|
1829
|
+
),
|
|
1830
|
+
);
|
|
1831
|
+
|
|
1832
|
+
// ----- editor view ----------------------------------------------
|
|
1833
|
+
const editorView =
|
|
1834
|
+
editor !== null
|
|
1835
|
+
? h(
|
|
1836
|
+
React.Fragment,
|
|
1837
|
+
null,
|
|
1838
|
+
h(
|
|
1839
|
+
'div',
|
|
1840
|
+
{ className: 'dsh-fk-toolbar' },
|
|
1841
|
+
h(
|
|
1842
|
+
'div',
|
|
1843
|
+
{ className: 'dsh-fk-crumbs' },
|
|
1844
|
+
h(
|
|
1845
|
+
'button',
|
|
1846
|
+
{ type: 'button', className: 'dsh-fk-crumb', onClick: closeEditor, title: t('back') },
|
|
1847
|
+
h(Icon, { name: 'chevronLeft', size: 13 }),
|
|
1848
|
+
t('back'),
|
|
1849
|
+
),
|
|
1850
|
+
h('span', { className: 'dsh-fk-crumb-sep' }, '/'),
|
|
1851
|
+
h('span', { className: 'dsh-fk-crumb dsh-fk-crumb-current', title: joinAbs(root, editor.rel) }, editor.rel),
|
|
1852
|
+
dirty ? h('span', { className: 'dsh-fk-badge' }, t('dirty')) : null,
|
|
1853
|
+
),
|
|
1854
|
+
h(
|
|
1855
|
+
'div',
|
|
1856
|
+
{ className: 'dsh-fk-toolbar-actions' },
|
|
1857
|
+
h(ToolButton, { icon: 'copy', label: t('copyRel'), onClick: () => copyRelPath(editor.rel) }),
|
|
1858
|
+
h(ToolButton, { icon: 'link', label: t('reference'), onClick: () => referenceRel(editor.rel) }),
|
|
1859
|
+
dirty
|
|
1860
|
+
? h(ToolButton, {
|
|
1861
|
+
icon: 'close',
|
|
1862
|
+
label: t('discard'),
|
|
1863
|
+
onClick: () => setDialog({ kind: 'discard', after: 'editor' }),
|
|
1864
|
+
})
|
|
1865
|
+
: null,
|
|
1866
|
+
h(ToolButton, {
|
|
1867
|
+
icon: 'check',
|
|
1868
|
+
label: saving ? t('saving') : t('save'),
|
|
1869
|
+
primary: true,
|
|
1870
|
+
disabled: saving || !dirty || editor.truncated,
|
|
1871
|
+
onClick: saveEditor,
|
|
1872
|
+
}),
|
|
1873
|
+
),
|
|
1874
|
+
),
|
|
1875
|
+
h(
|
|
1876
|
+
'div',
|
|
1877
|
+
{ className: 'dsh-fk-editor' },
|
|
1878
|
+
editor.truncated ? h('p', { className: 'dsh-fk-warn' }, h(Icon, { name: 'warning', size: 14 }), t('truncated')) : null,
|
|
1879
|
+
h(CodeEditor, {
|
|
1880
|
+
value: draft,
|
|
1881
|
+
onChange: setDraft,
|
|
1882
|
+
rel: editor.rel,
|
|
1883
|
+
fontSize: settingsSnapshot.editorFontSize,
|
|
1884
|
+
readOnly: editor.truncated,
|
|
1885
|
+
}),
|
|
1886
|
+
h(
|
|
1887
|
+
'div',
|
|
1888
|
+
{ className: 'dsh-fk-editor-meta' },
|
|
1889
|
+
h('span', null, `${formatBytes(editor.size)}${dirty ? ` → ${formatBytes(new TextEncoder().encode(draft).length)}` : ''}`),
|
|
1890
|
+
h('span', { className: 'dsh-fk-spacer' }),
|
|
1891
|
+
h('span', null, t('modified')),
|
|
1892
|
+
h('span', null, new Date(editor.mtimeMs).toLocaleString()),
|
|
1893
|
+
),
|
|
1894
|
+
),
|
|
1895
|
+
)
|
|
1896
|
+
: null;
|
|
1897
|
+
|
|
1898
|
+
// ----- browse view ----------------------------------------------
|
|
1899
|
+
const parts = cwd === '' ? [] : cwd.split('/');
|
|
1900
|
+
const rows = entries
|
|
1901
|
+
.filter((entry) => !isHiddenEntry(entry.name))
|
|
1902
|
+
.map((entry) =>
|
|
1903
|
+
h(
|
|
1904
|
+
'div',
|
|
1905
|
+
{
|
|
1906
|
+
key: entry.path,
|
|
1907
|
+
className: 'dsh-fk-row',
|
|
1908
|
+
'data-kind': entry.kind,
|
|
1909
|
+
'data-selected': selected === entry.path,
|
|
1910
|
+
'data-menu': menu !== null && menu.rel === entry.path,
|
|
1911
|
+
'data-drop': dropDir === entry.path,
|
|
1912
|
+
tabIndex: 0,
|
|
1913
|
+
draggable: true,
|
|
1914
|
+
onDragStart: (event) => startDrag(event, entry),
|
|
1915
|
+
onDragEnd: endDrag,
|
|
1916
|
+
onDragOver:
|
|
1917
|
+
entry.kind === 'directory'
|
|
1918
|
+
? (event) => {
|
|
1919
|
+
if (!canDropInto(draggingRef.current, entry.path)) return;
|
|
1920
|
+
event.preventDefault();
|
|
1921
|
+
event.stopPropagation();
|
|
1922
|
+
event.dataTransfer.dropEffect = 'move';
|
|
1923
|
+
setDropDir(entry.path);
|
|
1924
|
+
}
|
|
1925
|
+
: undefined,
|
|
1926
|
+
onDragLeave:
|
|
1927
|
+
entry.kind === 'directory'
|
|
1928
|
+
? (event) => {
|
|
1929
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
1930
|
+
if (dropDir === entry.path) setDropDir(null);
|
|
1931
|
+
}
|
|
1932
|
+
: undefined,
|
|
1933
|
+
onDrop:
|
|
1934
|
+
entry.kind === 'directory'
|
|
1935
|
+
? (event) => {
|
|
1936
|
+
event.preventDefault();
|
|
1937
|
+
event.stopPropagation();
|
|
1938
|
+
const source = draggingRef.current;
|
|
1939
|
+
endDrag();
|
|
1940
|
+
if (canDropInto(source, entry.path)) void moveRow(source.rel, source.kind, entry.path);
|
|
1941
|
+
}
|
|
1942
|
+
: undefined,
|
|
1943
|
+
onClick: () => {
|
|
1944
|
+
setSelected(entry.path);
|
|
1945
|
+
if (entry.kind === 'directory') {
|
|
1946
|
+
setCwd(entry.path);
|
|
1947
|
+
setMenu(null);
|
|
1948
|
+
} else {
|
|
1949
|
+
loadEditor(entry.path);
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
1952
|
+
onContextMenu: (event) => openRowMenu(event, entry),
|
|
1953
|
+
onKeyDown: (event) => {
|
|
1954
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1955
|
+
event.preventDefault();
|
|
1956
|
+
if (entry.kind === 'directory') setCwd(entry.path);
|
|
1957
|
+
else loadEditor(entry.path);
|
|
1958
|
+
}
|
|
1959
|
+
},
|
|
1960
|
+
title: entry.path,
|
|
1961
|
+
},
|
|
1962
|
+
h('span', { className: 'dsh-fk-rowicon', 'data-kind': entry.kind }, h(Icon, { name: entry.kind === 'directory' ? 'folder' : 'file', size: 15 })),
|
|
1963
|
+
h('span', { className: 'dsh-fk-rowname' }, entry.name),
|
|
1964
|
+
h('span', { className: 'dsh-fk-rowmeta' }, entry.kind === 'file' ? formatBytes(entry.size) : ''),
|
|
1965
|
+
h(
|
|
1966
|
+
'span',
|
|
1967
|
+
{ className: 'dsh-fk-rowactions' },
|
|
1968
|
+
h(IconButton, {
|
|
1969
|
+
icon: 'link',
|
|
1970
|
+
label: t('reference'),
|
|
1971
|
+
onClick: (event) => {
|
|
1972
|
+
event.stopPropagation();
|
|
1973
|
+
referenceRel(entry.path);
|
|
1974
|
+
},
|
|
1975
|
+
}),
|
|
1976
|
+
h(IconButton, {
|
|
1977
|
+
icon: 'more',
|
|
1978
|
+
label: t('open'),
|
|
1979
|
+
onClick: (event) => openRowMenu(event, entry),
|
|
1980
|
+
}),
|
|
1981
|
+
),
|
|
1982
|
+
),
|
|
1983
|
+
);
|
|
1984
|
+
|
|
1985
|
+
const browseView = h(
|
|
1986
|
+
React.Fragment,
|
|
1987
|
+
null,
|
|
1988
|
+
h(
|
|
1989
|
+
'div',
|
|
1990
|
+
{ className: 'dsh-fk-toolbar' },
|
|
1991
|
+
h(
|
|
1992
|
+
'div',
|
|
1993
|
+
{ className: 'dsh-fk-crumbs' },
|
|
1994
|
+
h(
|
|
1995
|
+
'button',
|
|
1996
|
+
{
|
|
1997
|
+
type: 'button',
|
|
1998
|
+
className: 'dsh-fk-crumb',
|
|
1999
|
+
title: root,
|
|
2000
|
+
onClick: () => setCwd(''),
|
|
2001
|
+
onDragOver: (event) => {
|
|
2002
|
+
if (!canDropInto(draggingRef.current, '')) return;
|
|
2003
|
+
event.preventDefault();
|
|
2004
|
+
setDropDir('');
|
|
2005
|
+
},
|
|
2006
|
+
onDrop: (event) => {
|
|
2007
|
+
event.preventDefault();
|
|
2008
|
+
const source = draggingRef.current;
|
|
2009
|
+
endDrag();
|
|
2010
|
+
if (canDropInto(source, '')) void moveRow(source.rel, source.kind, '');
|
|
2011
|
+
},
|
|
2012
|
+
'data-drop': dropDir === '',
|
|
2013
|
+
},
|
|
2014
|
+
h(Icon, { name: 'home', size: 13 }),
|
|
2015
|
+
workspaceName(root),
|
|
2016
|
+
),
|
|
2017
|
+
parts.map((segment, index) =>
|
|
2018
|
+
h(
|
|
2019
|
+
React.Fragment,
|
|
2020
|
+
{ key: `${index}-${segment}` },
|
|
2021
|
+
h(Icon, { name: 'chevronLeft', size: 11 }),
|
|
2022
|
+
h(
|
|
2023
|
+
'button',
|
|
2024
|
+
{ type: 'button', className: 'dsh-fk-crumb', onClick: () => setCwd(parts.slice(0, index + 1).join('/')) },
|
|
2025
|
+
segment,
|
|
2026
|
+
),
|
|
2027
|
+
),
|
|
2028
|
+
),
|
|
2029
|
+
),
|
|
2030
|
+
h(
|
|
2031
|
+
'div',
|
|
2032
|
+
{ className: 'dsh-fk-toolbar-actions' },
|
|
2033
|
+
h(ToolButton, { icon: 'filePlus', label: t('newFile'), onClick: () => setDialog({ kind: 'create-file' }) }),
|
|
2034
|
+
h(ToolButton, { icon: 'folderPlus', label: t('newDir'), onClick: () => setDialog({ kind: 'create-dir' }) }),
|
|
2035
|
+
h(ToolButton, { icon: 'trash', label: t('trash'), onClick: () => setView('trash') }),
|
|
2036
|
+
h(ToolButton, { icon: 'refresh', label: t('refresh'), onClick: refresh }),
|
|
2037
|
+
),
|
|
2038
|
+
),
|
|
2039
|
+
h(
|
|
2040
|
+
'div',
|
|
2041
|
+
{
|
|
2042
|
+
className: 'dsh-fk-body',
|
|
2043
|
+
onContextMenu: openBlankMenu,
|
|
2044
|
+
onDragOver: (event) => {
|
|
2045
|
+
if (typeof event.target.closest === 'function' && event.target.closest('.dsh-fk-row') !== null) return;
|
|
2046
|
+
if (!canDropInto(draggingRef.current, cwd)) return;
|
|
2047
|
+
event.preventDefault();
|
|
2048
|
+
setDropDir(cwd);
|
|
2049
|
+
},
|
|
2050
|
+
onDragLeave: (event) => {
|
|
2051
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
2052
|
+
if (dropDir === cwd) setDropDir(null);
|
|
2053
|
+
},
|
|
2054
|
+
onDrop: (event) => {
|
|
2055
|
+
if (typeof event.target.closest === 'function' && event.target.closest('.dsh-fk-row') !== null) return;
|
|
2056
|
+
event.preventDefault();
|
|
2057
|
+
const source = draggingRef.current;
|
|
2058
|
+
endDrag();
|
|
2059
|
+
if (canDropInto(source, cwd)) void moveRow(source.rel, source.kind, cwd);
|
|
2060
|
+
},
|
|
2061
|
+
},
|
|
2062
|
+
loading ? h('p', { className: 'dsh-fk-hint dsh-fk-center' }, t('loading')) : null,
|
|
2063
|
+
!loading && listError !== '' ? h('p', { className: 'dsh-fk-error dsh-fk-center' }, listError) : null,
|
|
2064
|
+
!loading && listError === '' && rows.length === 0 ? h('p', { className: 'dsh-fk-hint dsh-fk-center' }, t('empty')) : null,
|
|
2065
|
+
rows,
|
|
2066
|
+
),
|
|
2067
|
+
);
|
|
2068
|
+
|
|
2069
|
+
// ----- row menu content -----------------------------------------
|
|
2070
|
+
let menuItems = [];
|
|
2071
|
+
if (menu !== null) {
|
|
2072
|
+
if (menu.rel === null) {
|
|
2073
|
+
menuItems = [
|
|
2074
|
+
...(canPaste ? [{ label: t('paste'), icon: 'paste', onSelect: () => pasteInto(cwd) }] : []),
|
|
2075
|
+
{ label: t('newFile'), onSelect: () => setDialog({ kind: 'create-file' }) },
|
|
2076
|
+
{ label: t('newDir'), onSelect: () => setDialog({ kind: 'create-dir' }) },
|
|
2077
|
+
{ separator: true },
|
|
2078
|
+
{ label: t('refresh'), onSelect: refresh },
|
|
2079
|
+
];
|
|
2080
|
+
} else {
|
|
2081
|
+
menuItems = [
|
|
2082
|
+
...(menu.kind === 'file' ? [{ label: t('edit'), icon: 'edit', onSelect: () => loadEditor(menu.rel) }] : []),
|
|
2083
|
+
...(menu.kind === 'file' ? [{ label: t('openPreview'), icon: 'file', onSelect: () => openPreview(menu.rel) }] : []),
|
|
2084
|
+
...(menu.kind === 'directory' ? [{ label: t('newFileHere'), icon: 'filePlus', onSelect: () => setDialog({ kind: 'create-file', dir: menu.rel }) }] : []),
|
|
2085
|
+
...(menu.kind === 'directory' ? [{ label: t('newDirHere'), icon: 'folderPlus', onSelect: () => setDialog({ kind: 'create-dir', dir: menu.rel }) }] : []),
|
|
2086
|
+
...(menu.kind === 'directory' ? [{ separator: true }] : []),
|
|
2087
|
+
...(menu.kind === 'directory'
|
|
2088
|
+
? [{ label: t('pasteHere'), icon: 'paste', disabled: !canPaste, onSelect: () => pasteInto(menu.rel) }]
|
|
2089
|
+
: []),
|
|
2090
|
+
{ separator: true },
|
|
2091
|
+
{ label: t('rename'), icon: 'edit', onSelect: () => setDialog({ kind: 'rename', rel: menu.rel }) },
|
|
2092
|
+
{ label: t('move'), icon: 'folder', onSelect: () => setDialog({ kind: 'move', rel: menu.rel, entryKind: menu.kind }) },
|
|
2093
|
+
{ label: t('copy'), icon: 'copy', onSelect: () => copyToFileClipboard(root, menu.rel, menu.kind, 'copy') },
|
|
2094
|
+
{ label: t('cut'), icon: 'cut', onSelect: () => copyToFileClipboard(root, menu.rel, menu.kind, 'cut') },
|
|
2095
|
+
{ separator: true },
|
|
2096
|
+
{ label: t('copyRel'), icon: 'copy', onSelect: () => copyRelPath(menu.rel) },
|
|
2097
|
+
{ label: t('copyAbs'), icon: 'copy', onSelect: () => copyAbsPath(menu.rel) },
|
|
2098
|
+
{ label: t('reference'), icon: 'link', onSelect: () => referenceRel(menu.rel) },
|
|
2099
|
+
{ separator: true },
|
|
2100
|
+
{
|
|
2101
|
+
label: t('delete'),
|
|
2102
|
+
icon: 'trash',
|
|
2103
|
+
danger: true,
|
|
2104
|
+
onSelect: () => {
|
|
2105
|
+
if (settingsSnapshot.confirmDelete) setDialog({ kind: 'delete', rel: menu.rel });
|
|
2106
|
+
else mutate(() => api('delete', { root, path: menu.rel, sessionId }), t('deleted'));
|
|
2107
|
+
},
|
|
2108
|
+
},
|
|
2109
|
+
];
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
const menuNode = menu !== null ? h(MiniMenu, { x: menu.x, y: menu.y, items: menuItems, onClose: () => setMenu(null) }) : null;
|
|
2114
|
+
|
|
2115
|
+
return h(
|
|
2116
|
+
'div',
|
|
2117
|
+
{ className: 'dsh-fk-tab' },
|
|
2118
|
+
h(
|
|
2119
|
+
'div',
|
|
2120
|
+
{ className: 'dsh-fk-managerhead' },
|
|
2121
|
+
h('span', { className: 'dsh-fk-brandmark' }, h(Icon, { name: 'brand', size: 17 })),
|
|
2122
|
+
h(
|
|
2123
|
+
'span',
|
|
2124
|
+
{ className: 'dsh-fk-managertext' },
|
|
2125
|
+
h('strong', { className: 'dsh-fk-title' }, t('title')),
|
|
2126
|
+
h('span', { className: 'dsh-fk-sub', title: root }, root),
|
|
2127
|
+
),
|
|
2128
|
+
h('span', { className: 'dsh-fk-spacer' }),
|
|
2129
|
+
h(IconButton, {
|
|
2130
|
+
icon: 'panel',
|
|
2131
|
+
label: t('openFiles'),
|
|
2132
|
+
onClick: () => {
|
|
2133
|
+
try {
|
|
2134
|
+
tab.actions.openTab(FILES_KIND, { replaceTab: true });
|
|
2135
|
+
} catch (failure) {
|
|
2136
|
+
showToast(errorText(failure), 'error');
|
|
2137
|
+
}
|
|
2138
|
+
},
|
|
2139
|
+
}),
|
|
2140
|
+
h(IconButton, { icon: 'refresh', label: t('refresh'), onClick: refresh }),
|
|
2141
|
+
),
|
|
2142
|
+
editorView !== null ? editorView : view === 'trash' ? trashView : browseView,
|
|
2143
|
+
dragging !== null ? h('div', { className: 'dsh-fk-dragbar' }, h(Icon, { name: 'grip', size: 13 }), t('dragHint')) : null,
|
|
2144
|
+
notice !== null ? h('div', { className: 'dsh-fk-status', 'data-kind': notice.kind }, notice.text) : null,
|
|
2145
|
+
dialogNodes,
|
|
2146
|
+
menuNode,
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// ------------------------------------------------------------------
|
|
2151
|
+
// Workspace files tree (the `files` extension)
|
|
2152
|
+
// ------------------------------------------------------------------
|
|
2153
|
+
|
|
2154
|
+
function FilesBody(props) {
|
|
2155
|
+
const { useTabInfo, sessionId, inputActions, useSessions } = props;
|
|
2156
|
+
const { tab } = useTabInfo();
|
|
2157
|
+
const settingsSnapshot = useSettings();
|
|
2158
|
+
const clipboard = useFileClipboard();
|
|
2159
|
+
const [root, setRoot] = React.useState('');
|
|
2160
|
+
const [rootError, setRootError] = React.useState('');
|
|
2161
|
+
const [levels, setLevels] = React.useState({});
|
|
2162
|
+
const [expanded, setExpanded] = React.useState(['']);
|
|
2163
|
+
const [selected, setSelected] = React.useState(null);
|
|
2164
|
+
const [menu, setMenu] = React.useState(null);
|
|
2165
|
+
const [dialog, setDialog] = React.useState(null);
|
|
2166
|
+
const [notice, setNotice] = React.useState(null);
|
|
2167
|
+
const [dragging, setDragging] = React.useState(null);
|
|
2168
|
+
const [dropDir, setDropDir] = React.useState(null);
|
|
2169
|
+
const loadingRef = React.useRef(new Set());
|
|
2170
|
+
const draggingRef = React.useRef(null);
|
|
2171
|
+
const expandedRef = React.useRef(expanded);
|
|
2172
|
+
const levelsRef = React.useRef(levels);
|
|
2173
|
+
const rootGenerationRef = React.useRef(0);
|
|
2174
|
+
expandedRef.current = expanded;
|
|
2175
|
+
levelsRef.current = levels;
|
|
2176
|
+
|
|
2177
|
+
React.useEffect(() => {
|
|
2178
|
+
lastTabActions = tab.actions;
|
|
2179
|
+
}, [tab.actions]);
|
|
2180
|
+
|
|
2181
|
+
// Prefer the Session's own cwd (a synchronous value); fall back to the Host.
|
|
2182
|
+
const sessionCwd = useSessions((sessions) => sessions.byId[sessionId]?.cwd);
|
|
2183
|
+
|
|
2184
|
+
React.useEffect(() => {
|
|
2185
|
+
rootGenerationRef.current += 1;
|
|
2186
|
+
setLevels({});
|
|
2187
|
+
setExpanded(['']);
|
|
2188
|
+
setSelected(null);
|
|
2189
|
+
setMenu(null);
|
|
2190
|
+
setDialog(null);
|
|
2191
|
+
}, [root]);
|
|
2192
|
+
|
|
2193
|
+
React.useEffect(() => {
|
|
2194
|
+
let dead = false;
|
|
2195
|
+
setRootError('');
|
|
2196
|
+
if (typeof sessionCwd === 'string' && sessionCwd !== '') {
|
|
2197
|
+
setRoot(sessionCwd);
|
|
2198
|
+
return undefined;
|
|
2199
|
+
}
|
|
2200
|
+
if (typeof sessionId !== 'string' || sessionId === '') return undefined;
|
|
2201
|
+
api('workspace', { sessionId })
|
|
2202
|
+
.then((data) => {
|
|
2203
|
+
if (!dead) setRoot(data.root);
|
|
2204
|
+
})
|
|
2205
|
+
.catch((failure) => {
|
|
2206
|
+
if (!dead) setRootError(errorText(failure));
|
|
2207
|
+
});
|
|
2208
|
+
return () => {
|
|
2209
|
+
dead = true;
|
|
2210
|
+
};
|
|
2211
|
+
}, [sessionCwd, sessionId]);
|
|
2212
|
+
|
|
2213
|
+
React.useEffect(() => {
|
|
2214
|
+
if (notice === null) return undefined;
|
|
2215
|
+
const timer = setTimeout(() => setNotice(null), 4500);
|
|
2216
|
+
return () => clearTimeout(timer);
|
|
2217
|
+
}, [notice]);
|
|
2218
|
+
|
|
2219
|
+
const loadLevel = React.useCallback(
|
|
2220
|
+
async (dir) => {
|
|
2221
|
+
if (root === '') return;
|
|
2222
|
+
const generation = rootGenerationRef.current;
|
|
2223
|
+
const loadingKey = `${generation}|${dir}`;
|
|
2224
|
+
if (loadingRef.current.has(loadingKey)) return;
|
|
2225
|
+
loadingRef.current.add(loadingKey);
|
|
2226
|
+
setLevels((previous) => ({
|
|
2227
|
+
...previous,
|
|
2228
|
+
[dir]: { ...(previous[dir] ?? { entries: [] }), loading: true, error: '' },
|
|
2229
|
+
}));
|
|
2230
|
+
try {
|
|
2231
|
+
const data = await api('list', { root, path: dir, sessionId });
|
|
2232
|
+
if (rootGenerationRef.current !== generation) return;
|
|
2233
|
+
setLevels((previous) => ({
|
|
2234
|
+
...previous,
|
|
2235
|
+
[dir]: { loading: false, entries: data.entries, truncated: data.truncated === true },
|
|
2236
|
+
}));
|
|
2237
|
+
} catch (failure) {
|
|
2238
|
+
if (rootGenerationRef.current !== generation) return;
|
|
2239
|
+
setLevels((previous) => ({
|
|
2240
|
+
...previous,
|
|
2241
|
+
[dir]: { loading: false, entries: previous[dir]?.entries ?? [], error: errorText(failure) },
|
|
2242
|
+
}));
|
|
2243
|
+
} finally {
|
|
2244
|
+
loadingRef.current.delete(loadingKey);
|
|
2245
|
+
}
|
|
2246
|
+
},
|
|
2247
|
+
[root, sessionId],
|
|
2248
|
+
);
|
|
2249
|
+
|
|
2250
|
+
// Seed the root level.
|
|
2251
|
+
React.useEffect(() => {
|
|
2252
|
+
if (root === '') return;
|
|
2253
|
+
void loadLevel('');
|
|
2254
|
+
}, [root, loadLevel]);
|
|
2255
|
+
|
|
2256
|
+
// Load every expanded level that has never been fetched.
|
|
2257
|
+
React.useEffect(() => {
|
|
2258
|
+
if (root === '') return;
|
|
2259
|
+
for (const dir of expanded) {
|
|
2260
|
+
if (levelsRef.current[dir] === undefined) void loadLevel(dir);
|
|
2261
|
+
}
|
|
2262
|
+
}, [root, expanded, loadLevel]);
|
|
2263
|
+
|
|
2264
|
+
const reload = React.useCallback(() => {
|
|
2265
|
+
const dirs = new Set(['', ...expandedRef.current]);
|
|
2266
|
+
for (const dir of dirs) void loadLevel(dir);
|
|
2267
|
+
}, [loadLevel]);
|
|
2268
|
+
|
|
2269
|
+
// Page-refresh shortcut.
|
|
2270
|
+
React.useEffect(() => {
|
|
2271
|
+
if (typeof tab.actions?.bindCommands !== 'function') return undefined;
|
|
2272
|
+
return tab.actions.bindCommands({ refresh: reload });
|
|
2273
|
+
}, [tab.actions, reload]);
|
|
2274
|
+
|
|
2275
|
+
// Auto refresh expanded levels.
|
|
2276
|
+
React.useEffect(() => {
|
|
2277
|
+
if (root === '' || settingsSnapshot.autoRefreshSeconds <= 0) return undefined;
|
|
2278
|
+
const timer = setInterval(() => {
|
|
2279
|
+
if (document.visibilityState === 'hidden') return;
|
|
2280
|
+
const dirs = new Set(['', ...expandedRef.current]);
|
|
2281
|
+
for (const dir of dirs) void loadLevel(dir);
|
|
2282
|
+
}, settingsSnapshot.autoRefreshSeconds * 1000);
|
|
2283
|
+
return () => clearInterval(timer);
|
|
2284
|
+
}, [root, loadLevel, settingsSnapshot.autoRefreshSeconds]);
|
|
2285
|
+
|
|
2286
|
+
const toggleDir = (dir) => {
|
|
2287
|
+
setExpanded((previous) => (previous.includes(dir) ? previous.filter((value) => value !== dir) : [...previous, dir]));
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
const forgetSubtree = (rel) => {
|
|
2291
|
+
setExpanded((previous) => previous.filter((value) => value !== rel && !value.startsWith(rel + '/')));
|
|
2292
|
+
setLevels((previous) =>
|
|
2293
|
+
Object.fromEntries(Object.entries(previous).filter(([key]) => key !== rel && !key.startsWith(rel + '/'))),
|
|
2294
|
+
);
|
|
2295
|
+
};
|
|
2296
|
+
|
|
2297
|
+
const refreshDirs = async (dirs) => {
|
|
2298
|
+
const unique = new Set(dirs.filter((dir) => typeof dir === 'string'));
|
|
2299
|
+
await Promise.all([...unique].map((dir) => loadLevel(dir)));
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
const mutate = async (op, payload, successText, dirs, cleanup) => {
|
|
2303
|
+
if (root === '') return false;
|
|
2304
|
+
try {
|
|
2305
|
+
await api(op, { root, sessionId, ...payload });
|
|
2306
|
+
setNotice({ kind: 'ok', text: successText });
|
|
2307
|
+
if (cleanup !== undefined) cleanup();
|
|
2308
|
+
if (Array.isArray(dirs)) await refreshDirs(dirs);
|
|
2309
|
+
return true;
|
|
2310
|
+
} catch (failure) {
|
|
2311
|
+
setNotice({ kind: 'error', text: errorText(failure) });
|
|
2312
|
+
return false;
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
const renameEntry = (rel, kind, name) =>
|
|
2317
|
+
mutate('rename', { path: rel, name }, t('renamed'), [dirOf(rel)], () => {
|
|
2318
|
+
if (kind === 'directory') forgetSubtree(rel);
|
|
2319
|
+
if (selected === rel) setSelected(null);
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
const moveEntry = async (rel, kind, toDir) => {
|
|
2323
|
+
if (rel === '' || toDir === rel) return false;
|
|
2324
|
+
if (kind === 'directory' && toDir.startsWith(rel + '/')) {
|
|
2325
|
+
setNotice({ kind: 'error', text: t('error.bad-move') });
|
|
2326
|
+
return false;
|
|
2327
|
+
}
|
|
2328
|
+
if (dirOf(rel) === toDir) return true;
|
|
2329
|
+
return mutate('move', { path: rel, toDir }, t('moved'), [dirOf(rel), toDir], () => {
|
|
2330
|
+
if (kind === 'directory') forgetSubtree(rel);
|
|
2331
|
+
setSelected(joinRel(toDir, baseOf(rel)));
|
|
2332
|
+
});
|
|
2333
|
+
};
|
|
2334
|
+
|
|
2335
|
+
const deleteEntry = (rel, kind) =>
|
|
2336
|
+
mutate('delete', { path: rel }, t('deleted'), [dirOf(rel)], () => {
|
|
2337
|
+
if (kind === 'directory') forgetSubtree(rel);
|
|
2338
|
+
if (selected === rel) setSelected(null);
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
const createEntry = (dir, name, kind) => {
|
|
2342
|
+
const rel = joinRel(dir, name);
|
|
2343
|
+
return mutate('create', { path: rel, kind }, t('created'), [dir], () => {
|
|
2344
|
+
setSelected(rel);
|
|
2345
|
+
setExpanded((previous) => (dir === '' || previous.includes(dir) ? previous : [...previous, dir]));
|
|
2346
|
+
});
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
/** Paste the shared clipboard into one directory of this workspace. */
|
|
2350
|
+
const pasteInto = (toDir) => {
|
|
2351
|
+
const clip = fileClipboard;
|
|
2352
|
+
if (clip === null || clip.root !== root) return Promise.resolve(false);
|
|
2353
|
+
return mutate(
|
|
2354
|
+
clip.mode === 'cut' ? 'move' : 'copy',
|
|
2355
|
+
{ path: clip.rel, toDir, unique: true },
|
|
2356
|
+
t('pasted'),
|
|
2357
|
+
[dirOf(clip.rel), toDir],
|
|
2358
|
+
clip.mode === 'cut' ? () => setFileClipboard(null) : undefined,
|
|
2359
|
+
);
|
|
2360
|
+
};
|
|
2361
|
+
|
|
2362
|
+
const canPaste = clipboard !== null && clipboard.root === root;
|
|
2363
|
+
|
|
2364
|
+
const copyRelPath = async (rel) => {
|
|
2365
|
+
const ok = await copyText(rel);
|
|
2366
|
+
showToast(ok ? t('copiedPath') : t('error.internal'), ok ? 'ok' : 'error');
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2369
|
+
const copyAbsPath = async (rel) => {
|
|
2370
|
+
const ok = await copyText(joinAbs(root, rel));
|
|
2371
|
+
showToast(ok ? t('copiedPath') : t('error.internal'), ok ? 'ok' : 'error');
|
|
2372
|
+
};
|
|
2373
|
+
|
|
2374
|
+
const referenceRel = async (rel) => {
|
|
2375
|
+
const text = `@${rel} `;
|
|
2376
|
+
if (inputActions !== undefined && typeof inputActions.captureInsertion === 'function' && typeof inputActions.insertText === 'function') {
|
|
2377
|
+
try {
|
|
2378
|
+
const span = inputActions.captureInsertion();
|
|
2379
|
+
if (inputActions.insertText(text, span) === true) {
|
|
2380
|
+
inputActions.persistDraft?.();
|
|
2381
|
+
showToast(t('referenced'));
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
} catch {
|
|
2385
|
+
/* fall back to the clipboard */
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
const ok = await copyText(text);
|
|
2389
|
+
showToast(ok ? t('referencedCopied') : t('error.internal'), ok ? 'ok' : 'error');
|
|
2390
|
+
};
|
|
2391
|
+
|
|
2392
|
+
const openPreview = (rel) => {
|
|
2393
|
+
if (typeof tab.actions?.openResource !== 'function') return;
|
|
2394
|
+
try {
|
|
2395
|
+
tab.actions.openResource(fileAddress(sessionId, rel));
|
|
2396
|
+
} catch (failure) {
|
|
2397
|
+
showToast(errorText(failure), 'error');
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
const openFileKit = (params) => {
|
|
2402
|
+
if (typeof tab.actions?.openTab !== 'function') return;
|
|
2403
|
+
try {
|
|
2404
|
+
tab.actions.openTab(FILEKIT_KIND, { params });
|
|
2405
|
+
} catch (failure) {
|
|
2406
|
+
showToast(errorText(failure), 'error');
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
|
|
2410
|
+
const startDrag = (event, entry) => {
|
|
2411
|
+
event.stopPropagation();
|
|
2412
|
+
draggingRef.current = { rel: entry.path, kind: entry.kind };
|
|
2413
|
+
setDragging(draggingRef.current);
|
|
2414
|
+
try {
|
|
2415
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
2416
|
+
event.dataTransfer.setData('text/plain', entry.path);
|
|
2417
|
+
} catch {
|
|
2418
|
+
/* the ref carries the payload */
|
|
2419
|
+
}
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
const endDrag = () => {
|
|
2423
|
+
draggingRef.current = null;
|
|
2424
|
+
setDragging(null);
|
|
2425
|
+
setDropDir(null);
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
const dropInto = (toDir) => {
|
|
2429
|
+
const source = draggingRef.current;
|
|
2430
|
+
endDrag();
|
|
2431
|
+
if (!canDropInto(source, toDir)) return;
|
|
2432
|
+
void moveEntry(source.rel, source.kind, toDir);
|
|
2433
|
+
};
|
|
2434
|
+
|
|
2435
|
+
const openEntryMenu = (event, entry) => {
|
|
2436
|
+
event.preventDefault();
|
|
2437
|
+
event.stopPropagation();
|
|
2438
|
+
// The menu target is highlighted through `data-menu`; closing the menu
|
|
2439
|
+
// must not leave a selection behind (the row was never "selected").
|
|
2440
|
+
setMenu({ x: event.clientX, y: event.clientY, rel: entry.path, kind: entry.kind, name: entry.name });
|
|
2441
|
+
};
|
|
2442
|
+
|
|
2443
|
+
const openBlankMenu = (event) => {
|
|
2444
|
+
if (typeof event.target.closest === 'function' && event.target.closest('.dsh-fk-row, .dsh-fk-trow') !== null) return;
|
|
2445
|
+
event.preventDefault();
|
|
2446
|
+
setSelected(null);
|
|
2447
|
+
setMenu({ x: event.clientX, y: event.clientY, rel: null, kind: 'directory' });
|
|
2448
|
+
};
|
|
2449
|
+
|
|
2450
|
+
const menuItems = [];
|
|
2451
|
+
if (menu !== null) {
|
|
2452
|
+
if (menu.rel === null) {
|
|
2453
|
+
if (canPaste) menuItems.push({ label: t('paste'), icon: 'paste', onSelect: () => void pasteInto('') });
|
|
2454
|
+
menuItems.push(
|
|
2455
|
+
{ label: t('newFile'), icon: 'filePlus', onSelect: () => setDialog({ kind: 'create-file', dir: '' }) },
|
|
2456
|
+
{ label: t('newDir'), icon: 'folderPlus', onSelect: () => setDialog({ kind: 'create-dir', dir: '' }) },
|
|
2457
|
+
{ separator: true },
|
|
2458
|
+
{ label: t('openInFileKit'), icon: 'brand', onSelect: () => openFileKit({ dir: '' }) },
|
|
2459
|
+
{ label: t('openTrash'), icon: 'trash', onSelect: () => openFileKit({ view: 'trash' }) },
|
|
2460
|
+
{ separator: true },
|
|
2461
|
+
{ label: t('refresh'), icon: 'refresh', onSelect: reload },
|
|
2462
|
+
);
|
|
2463
|
+
} else {
|
|
2464
|
+
const isDir = menu.kind === 'directory';
|
|
2465
|
+
if (isDir) menuItems.push({ label: t('openDirInFileKit'), icon: 'brand', onSelect: () => openFileKit({ dir: menu.rel }) });
|
|
2466
|
+
else menuItems.push({ label: t('openPreview'), icon: 'file', onSelect: () => openPreview(menu.rel) });
|
|
2467
|
+
if (!isDir) menuItems.push({ label: t('openEditor'), icon: 'edit', onSelect: () => openFileKit({ rel: menu.rel, action: 'edit' }) });
|
|
2468
|
+
menuItems.push({ separator: true });
|
|
2469
|
+
if (isDir) {
|
|
2470
|
+
menuItems.push({ label: t('newFileHere'), icon: 'filePlus', onSelect: () => setDialog({ kind: 'create-file', dir: menu.rel }) });
|
|
2471
|
+
menuItems.push({ label: t('newDirHere'), icon: 'folderPlus', onSelect: () => setDialog({ kind: 'create-dir', dir: menu.rel }) });
|
|
2472
|
+
menuItems.push({ label: t('pasteHere'), icon: 'paste', disabled: !canPaste, onSelect: () => void pasteInto(menu.rel) });
|
|
2473
|
+
menuItems.push({ separator: true });
|
|
2474
|
+
}
|
|
2475
|
+
menuItems.push(
|
|
2476
|
+
{ label: t('rename'), icon: 'edit', onSelect: () => setDialog({ kind: 'rename', rel: menu.rel, entryKind: menu.kind }) },
|
|
2477
|
+
{ label: t('move'), icon: 'folder', onSelect: () => setDialog({ kind: 'move', rel: menu.rel, entryKind: menu.kind }) },
|
|
2478
|
+
{ label: t('copy'), icon: 'copy', onSelect: () => copyToFileClipboard(root, menu.rel, menu.kind, 'copy') },
|
|
2479
|
+
{ label: t('cut'), icon: 'cut', onSelect: () => copyToFileClipboard(root, menu.rel, menu.kind, 'cut') },
|
|
2480
|
+
{ separator: true },
|
|
2481
|
+
{ label: t('copyRel'), icon: 'copy', onSelect: () => void copyRelPath(menu.rel) },
|
|
2482
|
+
{ label: t('copyAbs'), icon: 'copy', onSelect: () => void copyAbsPath(menu.rel) },
|
|
2483
|
+
{ label: t('reference'), icon: 'link', onSelect: () => void referenceRel(menu.rel) },
|
|
2484
|
+
{ separator: true },
|
|
2485
|
+
{
|
|
2486
|
+
label: t('delete'),
|
|
2487
|
+
icon: 'trash',
|
|
2488
|
+
danger: true,
|
|
2489
|
+
onSelect: () => {
|
|
2490
|
+
if (settingsSnapshot.confirmDelete) setDialog({ kind: 'delete', rel: menu.rel, entryKind: menu.kind });
|
|
2491
|
+
else void deleteEntry(menu.rel, menu.kind);
|
|
2492
|
+
},
|
|
2493
|
+
},
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
const dialogNodes = [];
|
|
2499
|
+
if (dialog !== null) {
|
|
2500
|
+
if (dialog.kind === 'create-file' || dialog.kind === 'create-dir') {
|
|
2501
|
+
const isDir = dialog.kind === 'create-dir';
|
|
2502
|
+
dialogNodes.push(
|
|
2503
|
+
h(TextDialog, {
|
|
2504
|
+
key: isDir ? 'create-dir' : 'create-file',
|
|
2505
|
+
title: isDir ? t('newDir') : t('newFile'),
|
|
2506
|
+
label: t('name'),
|
|
2507
|
+
initial: '',
|
|
2508
|
+
placeholder: isDir ? 'assets' : 'notes.md',
|
|
2509
|
+
confirmText: t('create'),
|
|
2510
|
+
onClose: () => setDialog(null),
|
|
2511
|
+
onSubmit: (name) => createEntry(dialog.dir ?? '', name, isDir ? 'directory' : 'file'),
|
|
2512
|
+
}),
|
|
2513
|
+
);
|
|
2514
|
+
} else if (dialog.kind === 'rename') {
|
|
2515
|
+
dialogNodes.push(
|
|
2516
|
+
h(TextDialog, {
|
|
2517
|
+
key: 'rename',
|
|
2518
|
+
title: t('rename'),
|
|
2519
|
+
label: t('newName'),
|
|
2520
|
+
initial: baseOf(dialog.rel),
|
|
2521
|
+
confirmText: t('rename'),
|
|
2522
|
+
onClose: () => setDialog(null),
|
|
2523
|
+
onSubmit: (name) => renameEntry(dialog.rel, dialog.entryKind, name),
|
|
2524
|
+
}),
|
|
2525
|
+
);
|
|
2526
|
+
} else if (dialog.kind === 'move') {
|
|
2527
|
+
dialogNodes.push(
|
|
2528
|
+
h(MoveDialog, {
|
|
2529
|
+
key: 'move',
|
|
2530
|
+
root,
|
|
2531
|
+
rel: dialog.rel,
|
|
2532
|
+
sourceIsDir: dialog.entryKind === 'directory',
|
|
2533
|
+
onClose: () => setDialog(null),
|
|
2534
|
+
onMove: (toDir) => moveEntry(dialog.rel, dialog.entryKind, toDir),
|
|
2535
|
+
}),
|
|
2536
|
+
);
|
|
2537
|
+
} else if (dialog.kind === 'delete') {
|
|
2538
|
+
dialogNodes.push(
|
|
2539
|
+
h(ConfirmDialog, {
|
|
2540
|
+
key: 'delete',
|
|
2541
|
+
title: t('deleteTitle'),
|
|
2542
|
+
text: dialog.rel,
|
|
2543
|
+
hint: t('deleteHint', { trash: DSH_TRASH }),
|
|
2544
|
+
danger: true,
|
|
2545
|
+
confirmText: t('deleteConfirm'),
|
|
2546
|
+
onClose: () => setDialog(null),
|
|
2547
|
+
onConfirm: () => deleteEntry(dialog.rel, dialog.entryKind),
|
|
2548
|
+
}),
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
const visibleEntries = (level) => (level?.entries ?? []).filter((entry) => !isHiddenEntry(entry.name));
|
|
2554
|
+
|
|
2555
|
+
const renderLevel = (dir, depth) => {
|
|
2556
|
+
const level = levels[dir];
|
|
2557
|
+
const rows = [];
|
|
2558
|
+
if (level === undefined || (level.loading === true && (level.entries ?? []).length === 0)) {
|
|
2559
|
+
rows.push(h('p', { key: 'loading', className: 'dsh-fk-hint dsh-fk-treeline' }, t('loading')));
|
|
2560
|
+
} else {
|
|
2561
|
+
if (level.error) rows.push(h('p', { key: 'error', className: 'dsh-fk-error dsh-fk-treeline' }, level.error));
|
|
2562
|
+
const list = visibleEntries(level);
|
|
2563
|
+
if (list.length === 0 && !level.error) rows.push(h('p', { key: 'empty', className: 'dsh-fk-hint dsh-fk-treeline' }, t('empty')));
|
|
2564
|
+
for (const entry of list) {
|
|
2565
|
+
const isDir = entry.kind === 'directory';
|
|
2566
|
+
const open = isDir && expanded.includes(entry.path);
|
|
2567
|
+
rows.push(
|
|
2568
|
+
h(
|
|
2569
|
+
'div',
|
|
2570
|
+
{
|
|
2571
|
+
key: entry.path,
|
|
2572
|
+
className: 'dsh-fk-trow',
|
|
2573
|
+
'data-kind': entry.kind,
|
|
2574
|
+
'data-selected': selected === entry.path,
|
|
2575
|
+
'data-menu': menu !== null && menu.rel === entry.path,
|
|
2576
|
+
'data-drop': dropDir === entry.path,
|
|
2577
|
+
role: 'treeitem',
|
|
2578
|
+
'aria-expanded': isDir ? open : undefined,
|
|
2579
|
+
'aria-level': depth + 1,
|
|
2580
|
+
tabIndex: 0,
|
|
2581
|
+
style: { paddingLeft: 5 + depth * 14 },
|
|
2582
|
+
draggable: true,
|
|
2583
|
+
title: entry.path,
|
|
2584
|
+
onClick: () => {
|
|
2585
|
+
setSelected(entry.path);
|
|
2586
|
+
if (isDir) toggleDir(entry.path);
|
|
2587
|
+
else openPreview(entry.path);
|
|
2588
|
+
},
|
|
2589
|
+
onKeyDown: (event) => {
|
|
2590
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
2591
|
+
event.preventDefault();
|
|
2592
|
+
if (isDir) toggleDir(entry.path);
|
|
2593
|
+
else openPreview(entry.path);
|
|
2594
|
+
}
|
|
2595
|
+
},
|
|
2596
|
+
onContextMenu: (event) => openEntryMenu(event, entry),
|
|
2597
|
+
onDragStart: (event) => startDrag(event, entry),
|
|
2598
|
+
onDragEnd: endDrag,
|
|
2599
|
+
onDragOver: isDir
|
|
2600
|
+
? (event) => {
|
|
2601
|
+
if (!canDropInto(draggingRef.current, entry.path)) return;
|
|
2602
|
+
event.preventDefault();
|
|
2603
|
+
event.stopPropagation();
|
|
2604
|
+
event.dataTransfer.dropEffect = 'move';
|
|
2605
|
+
if (dropDir !== entry.path) setDropDir(entry.path);
|
|
2606
|
+
}
|
|
2607
|
+
: undefined,
|
|
2608
|
+
onDragLeave: isDir
|
|
2609
|
+
? (event) => {
|
|
2610
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
2611
|
+
if (dropDir === entry.path) setDropDir(null);
|
|
2612
|
+
}
|
|
2613
|
+
: undefined,
|
|
2614
|
+
onDrop: isDir
|
|
2615
|
+
? (event) => {
|
|
2616
|
+
event.preventDefault();
|
|
2617
|
+
event.stopPropagation();
|
|
2618
|
+
dropInto(entry.path);
|
|
2619
|
+
}
|
|
2620
|
+
: undefined,
|
|
2621
|
+
},
|
|
2622
|
+
h('span', { className: 'dsh-fk-tchev' }, isDir ? h(Icon, { name: open ? 'chevronDown' : 'chevronRight', size: 12 }) : null),
|
|
2623
|
+
h('span', { className: 'dsh-fk-trowicon', 'data-kind': entry.kind }, h(Icon, { name: isDir ? (open ? 'folderOpen' : 'folder') : 'file', size: 15 })),
|
|
2624
|
+
h('span', { className: 'dsh-fk-trowname' }, entry.name),
|
|
2625
|
+
h('span', { className: 'dsh-fk-trowmeta' }, entry.kind === 'file' ? formatBytes(entry.size) : ''),
|
|
2626
|
+
),
|
|
2627
|
+
);
|
|
2628
|
+
if (open) rows.push(renderLevel(entry.path, depth + 1));
|
|
2629
|
+
}
|
|
2630
|
+
if (level.truncated === true) rows.push(h('p', { key: 'truncated', className: 'dsh-fk-hint dsh-fk-treeline' }, t('truncatedEntries')));
|
|
2631
|
+
}
|
|
2632
|
+
return h('div', { key: 'level:' + (dir === '' ? 'root' : dir), className: 'dsh-fk-tlevel' }, rows);
|
|
2633
|
+
};
|
|
2634
|
+
|
|
2635
|
+
const body = h(
|
|
2636
|
+
'div',
|
|
2637
|
+
{ className: 'dsh-fk-tab dsh-fk-filestab' },
|
|
2638
|
+
h(
|
|
2639
|
+
'div',
|
|
2640
|
+
{ className: 'dsh-fk-treehead' },
|
|
2641
|
+
h(
|
|
2642
|
+
'button',
|
|
2643
|
+
{
|
|
2644
|
+
type: 'button',
|
|
2645
|
+
className: 'dsh-fk-rootpath',
|
|
2646
|
+
title: root,
|
|
2647
|
+
onClick: () => setSelected(null),
|
|
2648
|
+
onDragOver: (event) => {
|
|
2649
|
+
if (!canDropInto(draggingRef.current, '')) return;
|
|
2650
|
+
event.preventDefault();
|
|
2651
|
+
setDropDir('');
|
|
2652
|
+
},
|
|
2653
|
+
onDragLeave: () => {
|
|
2654
|
+
if (dropDir === '') setDropDir(null);
|
|
2655
|
+
},
|
|
2656
|
+
onDrop: (event) => {
|
|
2657
|
+
event.preventDefault();
|
|
2658
|
+
dropInto('');
|
|
2659
|
+
},
|
|
2660
|
+
'data-drop': dropDir === '',
|
|
2661
|
+
},
|
|
2662
|
+
h(Icon, { name: 'home', size: 13 }),
|
|
2663
|
+
h('span', { className: 'dsh-fk-rootpathtext' }, root === '' ? t('loading') : root),
|
|
2664
|
+
),
|
|
2665
|
+
h('span', { className: 'dsh-fk-spacer' }),
|
|
2666
|
+
h(IconButton, { icon: 'refresh', label: t('refresh'), onClick: reload }),
|
|
2667
|
+
h(IconButton, { icon: 'brand', label: t('openTitle'), onClick: () => openFileKitTab({ dir: '' }) }),
|
|
2668
|
+
),
|
|
2669
|
+
h(
|
|
2670
|
+
'div',
|
|
2671
|
+
{
|
|
2672
|
+
className: 'dsh-fk-treebody',
|
|
2673
|
+
role: 'tree',
|
|
2674
|
+
'aria-label': t('filesGuideTitle'),
|
|
2675
|
+
onContextMenu: openBlankMenu,
|
|
2676
|
+
onDragOver: (event) => {
|
|
2677
|
+
if (typeof event.target.closest === 'function' && event.target.closest('.dsh-fk-trow') !== null) return;
|
|
2678
|
+
if (!canDropInto(draggingRef.current, '')) return;
|
|
2679
|
+
event.preventDefault();
|
|
2680
|
+
setDropDir('');
|
|
2681
|
+
},
|
|
2682
|
+
onDragLeave: (event) => {
|
|
2683
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
2684
|
+
if (dropDir === '') setDropDir(null);
|
|
2685
|
+
},
|
|
2686
|
+
onDrop: (event) => {
|
|
2687
|
+
if (typeof event.target.closest === 'function' && event.target.closest('.dsh-fk-trow') !== null) return;
|
|
2688
|
+
event.preventDefault();
|
|
2689
|
+
dropInto('');
|
|
2690
|
+
},
|
|
2691
|
+
},
|
|
2692
|
+
rootError !== '' ? h('p', { className: 'dsh-fk-error dsh-fk-center' }, rootError) : null,
|
|
2693
|
+
root === '' && rootError === '' ? h('p', { className: 'dsh-fk-hint dsh-fk-center' }, t('loading')) : null,
|
|
2694
|
+
root !== '' ? renderLevel('', 0) : null,
|
|
2695
|
+
dragging !== null
|
|
2696
|
+
? h(
|
|
2697
|
+
'div',
|
|
2698
|
+
{
|
|
2699
|
+
className: 'dsh-fk-rootdrop',
|
|
2700
|
+
'data-over': dropDir === '',
|
|
2701
|
+
onDragOver: (event) => {
|
|
2702
|
+
if (!canDropInto(draggingRef.current, '')) return;
|
|
2703
|
+
event.preventDefault();
|
|
2704
|
+
event.stopPropagation();
|
|
2705
|
+
setDropDir('');
|
|
2706
|
+
},
|
|
2707
|
+
onDrop: (event) => {
|
|
2708
|
+
event.preventDefault();
|
|
2709
|
+
event.stopPropagation();
|
|
2710
|
+
dropInto('');
|
|
2711
|
+
},
|
|
2712
|
+
},
|
|
2713
|
+
h(Icon, { name: 'move', size: 14 }),
|
|
2714
|
+
t('dropRoot'),
|
|
2715
|
+
)
|
|
2716
|
+
: null,
|
|
2717
|
+
),
|
|
2718
|
+
h(
|
|
2719
|
+
'div',
|
|
2720
|
+
{ className: 'dsh-fk-treefoot' },
|
|
2721
|
+
h(Icon, { name: 'grip', size: 12 }),
|
|
2722
|
+
h('span', null, t('dragHint')),
|
|
2723
|
+
h('span', { className: 'dsh-fk-spacer' }),
|
|
2724
|
+
notice !== null ? h('span', { className: 'dsh-fk-footnotice', 'data-kind': notice.kind }, notice.text) : null,
|
|
2725
|
+
),
|
|
2726
|
+
dialogNodes,
|
|
2727
|
+
menu !== null ? h(MiniMenu, { x: menu.x, y: menu.y, items: menuItems, onClose: () => setMenu(null) }) : null,
|
|
2728
|
+
);
|
|
2729
|
+
return body;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// ------------------------------------------------------------------
|
|
2733
|
+
// Titles
|
|
2734
|
+
// ------------------------------------------------------------------
|
|
2735
|
+
|
|
2736
|
+
function FilesTitle({ useTabInfo }) {
|
|
2737
|
+
const { tab } = useTabInfo();
|
|
2738
|
+
return h('span', { className: 'dsh-fk-tabtitle' }, h(Icon, { name: 'folder', size: 15 }), tab.title);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
function FileKitTitle({ useTabInfo }) {
|
|
2742
|
+
const { tab } = useTabInfo();
|
|
2743
|
+
return h('span', { className: 'dsh-fk-tabtitle' }, h(Icon, { name: 'brand', size: 15 }), tab.title);
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// ------------------------------------------------------------------
|
|
2747
|
+
// Guide card for the workspace-files entry
|
|
2748
|
+
// ------------------------------------------------------------------
|
|
2749
|
+
|
|
2750
|
+
function FilesGuideCard(props) {
|
|
2751
|
+
const { useTabInfo, kind, title, description } = props;
|
|
2752
|
+
const { tab } = useTabInfo();
|
|
2753
|
+
const openFiles = () => {
|
|
2754
|
+
try {
|
|
2755
|
+
tab.actions.openTab(kind, { replaceTab: true });
|
|
2756
|
+
} catch (failure) {
|
|
2757
|
+
showToast(errorText(failure), 'error');
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
const openFileKit = () => {
|
|
2761
|
+
try {
|
|
2762
|
+
tab.actions.openTab(FILEKIT_KIND, { replaceTab: true });
|
|
2763
|
+
} catch (failure) {
|
|
2764
|
+
showToast(errorText(failure), 'error');
|
|
2765
|
+
}
|
|
2766
|
+
};
|
|
2767
|
+
return h(
|
|
2768
|
+
'div',
|
|
2769
|
+
{ className: 'dsh-fk-guide', 'data-filekit-guide': true },
|
|
2770
|
+
h(
|
|
2771
|
+
'button',
|
|
2772
|
+
{ type: 'button', className: 'dsh-fk-guidecard', onClick: openFiles, 'data-sidebar-right-guide-entry': kind },
|
|
2773
|
+
h('span', { className: 'dsh-fk-guideicon' }, h(Icon, { name: 'folder', size: 26 })),
|
|
2774
|
+
h(
|
|
2775
|
+
'span',
|
|
2776
|
+
{ className: 'dsh-fk-guidetext' },
|
|
2777
|
+
h('span', { className: 'dsh-fk-guidetitle' }, title),
|
|
2778
|
+
description !== undefined ? h('span', { className: 'dsh-fk-guidedesc' }, description) : null,
|
|
2779
|
+
),
|
|
2780
|
+
h(Icon, { name: 'chevronRight', size: 14 }),
|
|
2781
|
+
),
|
|
2782
|
+
h('div', { className: 'dsh-fk-guidesep', role: 'none' }),
|
|
2783
|
+
h(
|
|
2784
|
+
'button',
|
|
2785
|
+
{ type: 'button', className: 'dsh-fk-guideextra', onClick: openFileKit, title: t('openTitle'), 'data-filekit-open': true },
|
|
2786
|
+
h('span', { className: 'dsh-fk-guideextraicon' }, h(Icon, { name: 'brand', size: 15 })),
|
|
2787
|
+
h(
|
|
2788
|
+
'span',
|
|
2789
|
+
{ className: 'dsh-fk-guidetext' },
|
|
2790
|
+
h('span', { className: 'dsh-fk-guidetitle' }, t('guideExtra')),
|
|
2791
|
+
h('span', { className: 'dsh-fk-guidedesc' }, t('guideExtraHint')),
|
|
2792
|
+
),
|
|
2793
|
+
h(Icon, { name: 'chevronRight', size: 14 }),
|
|
2794
|
+
),
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// ------------------------------------------------------------------
|
|
2799
|
+
// Files/documents header actions
|
|
2800
|
+
// ------------------------------------------------------------------
|
|
2801
|
+
|
|
2802
|
+
let sidebarRightService = undefined;
|
|
2803
|
+
let lastTabActions = undefined;
|
|
2804
|
+
|
|
2805
|
+
function openFileKitTab(params) {
|
|
2806
|
+
const options = params === undefined ? {} : { params };
|
|
2807
|
+
const actions = lastTabActions;
|
|
2808
|
+
try {
|
|
2809
|
+
if (sidebarRightService !== undefined && typeof sidebarRightService.openTab === 'function') {
|
|
2810
|
+
sidebarRightService.openTab(FILEKIT_KIND, options);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
if (actions !== undefined && typeof actions.openTab === 'function') {
|
|
2814
|
+
actions.openTab(FILEKIT_KIND, options);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
} catch (failure) {
|
|
2818
|
+
showToast(errorText(failure), 'error');
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
showToast(t('error.internal'), 'error');
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
async function insertReference(abs, sessionId, inputActions) {
|
|
2825
|
+
const workspace = await api('workspace', { sessionId, file: abs });
|
|
2826
|
+
const rel = relFromAbs(abs, workspace.root);
|
|
2827
|
+
if (rel === null) throw Object.assign(new Error('outside'), { code: 'outside-workspace' });
|
|
2828
|
+
const text = `@${rel} `;
|
|
2829
|
+
if (inputActions !== undefined && typeof inputActions.captureInsertion === 'function' && typeof inputActions.insertText === 'function') {
|
|
2830
|
+
try {
|
|
2831
|
+
const span = inputActions.captureInsertion();
|
|
2832
|
+
if (inputActions.insertText(text, span) === true) {
|
|
2833
|
+
inputActions.persistDraft?.();
|
|
2834
|
+
showToast(t('referenced'));
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
} catch {
|
|
2838
|
+
/* fall back to the clipboard */
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
if (await copyText(text)) showToast(t('referencedCopied'));
|
|
2842
|
+
else showToast(t('error.internal'), 'error');
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
async function openFileInKit(abs, sessionId, inputActions, action) {
|
|
2846
|
+
const workspace = await api('workspace', { sessionId, file: abs });
|
|
2847
|
+
const rel = relFromAbs(abs, workspace.root);
|
|
2848
|
+
if (rel === null) throw Object.assign(new Error('outside'), { code: 'outside-workspace' });
|
|
2849
|
+
openFileKitTab({ rel, action });
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
function DocumentActions(props) {
|
|
2853
|
+
const abs = typeof props.absolutePath === 'string' ? props.absolutePath : '';
|
|
2854
|
+
const sessionId = props.sessionId;
|
|
2855
|
+
const inputActions = props.inputActions;
|
|
2856
|
+
const [menu, setMenu] = React.useState(null);
|
|
2857
|
+
if (abs === '') return null;
|
|
2858
|
+
|
|
2859
|
+
const run = async (label, fn) => {
|
|
2860
|
+
try {
|
|
2861
|
+
await fn();
|
|
2862
|
+
} catch (error) {
|
|
2863
|
+
showToast(`${label}${translate('errorPrefix')}${errorText(error)}`, 'error');
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
|
|
2867
|
+
const openMenu = (event) => {
|
|
2868
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
2869
|
+
setMenu({ x: rect.right - 224, y: rect.bottom + 4 });
|
|
2870
|
+
};
|
|
2871
|
+
|
|
2872
|
+
const items = [
|
|
2873
|
+
{ label: t('openEditor'), icon: 'edit', onSelect: () => run(t('openEditor'), () => openFileInKit(abs, sessionId, inputActions, 'edit')) },
|
|
2874
|
+
{ label: t('rename'), icon: 'edit', onSelect: () => run(t('rename'), () => openFileInKit(abs, sessionId, inputActions, 'rename')) },
|
|
2875
|
+
{ label: t('move'), icon: 'folder', onSelect: () => run(t('move'), () => openFileInKit(abs, sessionId, inputActions, 'move')) },
|
|
2876
|
+
{ label: t('copyAbs'), icon: 'copy', onSelect: () => run(t('copyAbs'), async () => {
|
|
2877
|
+
if (await copyText(abs)) showToast(t('copiedPath'));
|
|
2878
|
+
else showToast(t('error.internal'), 'error');
|
|
2879
|
+
}) },
|
|
2880
|
+
{ label: t('copyRel'), icon: 'copy', onSelect: () => run(t('copyRel'), async () => {
|
|
2881
|
+
const workspace = await api('workspace', { sessionId, file: abs });
|
|
2882
|
+
const rel = relFromAbs(abs, workspace.root);
|
|
2883
|
+
if (rel === null) throw Object.assign(new Error('outside'), { code: 'outside-workspace' });
|
|
2884
|
+
if (await copyText(rel)) showToast(t('copiedPath'));
|
|
2885
|
+
else showToast(t('error.internal'), 'error');
|
|
2886
|
+
}) },
|
|
2887
|
+
{ label: t('reference'), icon: 'link', onSelect: () => run(t('reference'), () => insertReference(abs, sessionId, inputActions)) },
|
|
2888
|
+
{ separator: true },
|
|
2889
|
+
{ label: t('delete'), icon: 'trash', danger: true, onSelect: () => run(t('delete'), () => openFileInKit(abs, sessionId, inputActions, 'delete')) },
|
|
2890
|
+
];
|
|
2891
|
+
|
|
2892
|
+
return h(
|
|
2893
|
+
'span',
|
|
2894
|
+
{ className: 'dsh-fk-doc-actions' },
|
|
2895
|
+
h(
|
|
2896
|
+
'button',
|
|
2897
|
+
{
|
|
2898
|
+
type: 'button',
|
|
2899
|
+
className: 'dsh-fk-tool',
|
|
2900
|
+
title: t('edit'),
|
|
2901
|
+
'aria-label': t('edit'),
|
|
2902
|
+
onClick: () => run(t('edit'), () => openFileInKit(abs, sessionId, inputActions, 'edit')),
|
|
2903
|
+
},
|
|
2904
|
+
h(Icon, { name: 'edit', size: 15 }),
|
|
2905
|
+
),
|
|
2906
|
+
h(
|
|
2907
|
+
'button',
|
|
2908
|
+
{
|
|
2909
|
+
type: 'button',
|
|
2910
|
+
className: 'dsh-fk-tool',
|
|
2911
|
+
title: t('reference'),
|
|
2912
|
+
'aria-label': t('reference'),
|
|
2913
|
+
onClick: () => run(t('reference'), () => insertReference(abs, sessionId, inputActions)),
|
|
2914
|
+
},
|
|
2915
|
+
h(Icon, { name: 'link', size: 15 }),
|
|
2916
|
+
),
|
|
2917
|
+
h(
|
|
2918
|
+
'button',
|
|
2919
|
+
{
|
|
2920
|
+
type: 'button',
|
|
2921
|
+
className: 'dsh-fk-tool',
|
|
2922
|
+
title: t('open'),
|
|
2923
|
+
'aria-label': t('open'),
|
|
2924
|
+
'aria-haspopup': 'menu',
|
|
2925
|
+
onClick: openMenu,
|
|
2926
|
+
},
|
|
2927
|
+
h(Icon, { name: 'more', size: 15 }),
|
|
2928
|
+
),
|
|
2929
|
+
menu !== null ? h(MiniMenu, { x: menu.x, y: menu.y, items, onClose: () => setMenu(null) }) : null,
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// ------------------------------------------------------------------
|
|
2934
|
+
// Settings page
|
|
2935
|
+
// ------------------------------------------------------------------
|
|
2936
|
+
|
|
2937
|
+
function ToggleRow({ title, hint, checked, onChange }) {
|
|
2938
|
+
return h(
|
|
2939
|
+
'div',
|
|
2940
|
+
{ className: 'dsh-fk-setrow' },
|
|
2941
|
+
h(
|
|
2942
|
+
'div',
|
|
2943
|
+
{ className: 'dsh-fk-settext' },
|
|
2944
|
+
h('span', { className: 'dsh-fk-settitle' }, title),
|
|
2945
|
+
hint !== undefined ? h('span', { className: 'dsh-fk-sethint' }, hint) : null,
|
|
2946
|
+
),
|
|
2947
|
+
h(
|
|
2948
|
+
'button',
|
|
2949
|
+
{
|
|
2950
|
+
type: 'button',
|
|
2951
|
+
className: 'dsh-fk-switch',
|
|
2952
|
+
role: 'switch',
|
|
2953
|
+
'aria-checked': checked,
|
|
2954
|
+
'data-on': checked,
|
|
2955
|
+
onClick: () => onChange(!checked),
|
|
2956
|
+
},
|
|
2957
|
+
h('span', { className: 'dsh-fk-switchknob' }),
|
|
2958
|
+
),
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
function FileKitSettings() {
|
|
2963
|
+
const snapshot = useSettings();
|
|
2964
|
+
const [flash, setFlash] = React.useState('');
|
|
2965
|
+
React.useEffect(() => {
|
|
2966
|
+
if (flash === '') return undefined;
|
|
2967
|
+
const timer = setTimeout(() => setFlash(''), 2600);
|
|
2968
|
+
return () => clearTimeout(timer);
|
|
2969
|
+
}, [flash]);
|
|
2970
|
+
const reset = () => {
|
|
2971
|
+
resetSettings();
|
|
2972
|
+
setFlash(t('settingsResetDone'));
|
|
2973
|
+
};
|
|
2974
|
+
return h(
|
|
2975
|
+
'div',
|
|
2976
|
+
{ className: 'dsh-fk-settings' },
|
|
2977
|
+
h(
|
|
2978
|
+
'div',
|
|
2979
|
+
{ className: 'dsh-fk-sethead' },
|
|
2980
|
+
h('span', { className: 'dsh-fk-setbrand' }, h(Icon, { name: 'brand', size: 22 })),
|
|
2981
|
+
h(
|
|
2982
|
+
'span',
|
|
2983
|
+
{ className: 'dsh-fk-setheadtext' },
|
|
2984
|
+
h('strong', { className: 'dsh-fk-settitle-main' }, t('settingsTitle')),
|
|
2985
|
+
h('span', { className: 'dsh-fk-sethint' }, t('settingsIntro')),
|
|
2986
|
+
),
|
|
2987
|
+
),
|
|
2988
|
+
h(ToggleRow, {
|
|
2989
|
+
title: t('settingsShowHidden'),
|
|
2990
|
+
hint: t('settingsShowHiddenHint'),
|
|
2991
|
+
checked: snapshot.showHidden,
|
|
2992
|
+
onChange: (value) => updateSettings({ showHidden: value }),
|
|
2993
|
+
}),
|
|
2994
|
+
h(ToggleRow, {
|
|
2995
|
+
title: t('settingsNotify'),
|
|
2996
|
+
hint: t('settingsNotifyHint'),
|
|
2997
|
+
checked: snapshot.notifyAgent,
|
|
2998
|
+
onChange: (value) => updateSettings({ notifyAgent: value }),
|
|
2999
|
+
}),
|
|
3000
|
+
h(ToggleRow, {
|
|
3001
|
+
title: t('settingsConfirmDelete'),
|
|
3002
|
+
hint: t('settingsConfirmDeleteHint'),
|
|
3003
|
+
checked: snapshot.confirmDelete,
|
|
3004
|
+
onChange: (value) => updateSettings({ confirmDelete: value }),
|
|
3005
|
+
}),
|
|
3006
|
+
h(
|
|
3007
|
+
'div',
|
|
3008
|
+
{ className: 'dsh-fk-setrow' },
|
|
3009
|
+
h(
|
|
3010
|
+
'div',
|
|
3011
|
+
{ className: 'dsh-fk-settext' },
|
|
3012
|
+
h('span', { className: 'dsh-fk-settitle' }, t('settingsAutoRefresh')),
|
|
3013
|
+
h('span', { className: 'dsh-fk-sethint' }, t('settingsAutoRefreshHint')),
|
|
3014
|
+
),
|
|
3015
|
+
h(
|
|
3016
|
+
'div',
|
|
3017
|
+
{ className: 'dsh-fk-segmented', role: 'group' },
|
|
3018
|
+
AUTO_REFRESH_CHOICES.map((seconds) =>
|
|
3019
|
+
h(
|
|
3020
|
+
'button',
|
|
3021
|
+
{
|
|
3022
|
+
key: seconds,
|
|
3023
|
+
type: 'button',
|
|
3024
|
+
className: 'dsh-fk-segment',
|
|
3025
|
+
'data-on': snapshot.autoRefreshSeconds === seconds,
|
|
3026
|
+
onClick: () => updateSettings({ autoRefreshSeconds: seconds }),
|
|
3027
|
+
},
|
|
3028
|
+
seconds === 0
|
|
3029
|
+
? t('settingsAutoRefreshOff')
|
|
3030
|
+
: seconds === 15
|
|
3031
|
+
? t('settingsAutoRefresh15')
|
|
3032
|
+
: seconds === 30
|
|
3033
|
+
? t('settingsAutoRefresh30')
|
|
3034
|
+
: t('settingsAutoRefresh60'),
|
|
3035
|
+
),
|
|
3036
|
+
),
|
|
3037
|
+
),
|
|
3038
|
+
),
|
|
3039
|
+
h(
|
|
3040
|
+
'div',
|
|
3041
|
+
{ className: 'dsh-fk-setrow' },
|
|
3042
|
+
h(
|
|
3043
|
+
'div',
|
|
3044
|
+
{ className: 'dsh-fk-settext' },
|
|
3045
|
+
h('span', { className: 'dsh-fk-settitle' }, t('settingsFontSize')),
|
|
3046
|
+
h('span', { className: 'dsh-fk-sethint' }, t('settingsFontSizeHint')),
|
|
3047
|
+
),
|
|
3048
|
+
h(
|
|
3049
|
+
'div',
|
|
3050
|
+
{ className: 'dsh-fk-fontpicker' },
|
|
3051
|
+
h('input', {
|
|
3052
|
+
className: 'dsh-fk-range',
|
|
3053
|
+
type: 'range',
|
|
3054
|
+
min: FONT_MIN,
|
|
3055
|
+
max: FONT_MAX,
|
|
3056
|
+
step: 1,
|
|
3057
|
+
value: snapshot.editorFontSize,
|
|
3058
|
+
onChange: (event) => updateSettings({ editorFontSize: clampFontSize(event.target.value) }),
|
|
3059
|
+
'aria-label': t('settingsFontSize'),
|
|
3060
|
+
}),
|
|
3061
|
+
h('span', { className: 'dsh-fk-fontvalue' }, `${snapshot.editorFontSize}px`),
|
|
3062
|
+
),
|
|
3063
|
+
),
|
|
3064
|
+
h(
|
|
3065
|
+
'div',
|
|
3066
|
+
{ className: 'dsh-fk-setfoot' },
|
|
3067
|
+
h('button', { type: 'button', className: 'dsh-fk-btn', onClick: reset }, h(Icon, { name: 'restore', size: 14 }), h('span', null, t('settingsReset'))),
|
|
3068
|
+
flash !== '' ? h('span', { className: 'dsh-fk-setflash' }, flash) : null,
|
|
3069
|
+
),
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// ------------------------------------------------------------------
|
|
3074
|
+
// Styles — gold → orange-red brand accents
|
|
3075
|
+
// ------------------------------------------------------------------
|
|
3076
|
+
|
|
3077
|
+
const CSS = `
|
|
3078
|
+
:root{--dsh-fk-gold:#f5b83d;--dsh-fk-orange:#f4511e;--dsh-fk-grad:linear-gradient(135deg,#f7c948 0%,#f4932a 52%,#f4511e 100%);--dsh-fk-dir:#c47a12;--dsh-fk-soft:rgb(244 81 30/.12);--dsh-fk-hover:var(--dsw-alias-interactive-bg-hover,rgb(0 0 0/.055));--dsh-fk-active:var(--dsw-alias-interactive-bg-active,rgb(0 0 0/.09));--dsh-fk-editor-font:12px}
|
|
3079
|
+
body[data-ds-dark-theme]{--dsh-fk-dir:#f3bf4d;--dsh-fk-soft:rgb(247 201 72/.14)}
|
|
3080
|
+
.dsh-fk-tool{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;flex:none}
|
|
3081
|
+
.dsh-fk-tool:hover{background:var(--dsh-fk-hover);color:var(--dsh-fk-orange)}
|
|
3082
|
+
.dsh-fk-doc-actions{display:inline-flex;align-items:center;gap:1px;flex:none}
|
|
3083
|
+
.dsh-fk-tab{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:0;overflow:hidden;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;background:var(--dsw-alias-bg-base)}
|
|
3084
|
+
.dsh-fk-tabstatus{display:flex;flex-direction:column;gap:12px;align-items:flex-start;justify-content:center;height:100%;padding:24px}
|
|
3085
|
+
.dsh-fk-managerhead{display:flex;align-items:center;gap:9px;padding:9px 10px 9px 12px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
|
|
3086
|
+
.dsh-fk-brandmark{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:8px;background:var(--dsh-fk-grad);color:#fff;flex:none;box-shadow:0 2px 8px rgb(244 81 30/.28)}
|
|
3087
|
+
.dsh-fk-managertext{display:flex;flex-direction:column;min-width:0;gap:1px}
|
|
3088
|
+
.dsh-fk-title{font-size:14px;font-weight:650;line-height:18px}
|
|
3089
|
+
.dsh-fk-sub{font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:min(360px,52vw)}
|
|
3090
|
+
.dsh-fk-spacer{flex:1}
|
|
3091
|
+
.dsh-fk-toolbar{display:flex;align-items:center;gap:8px;padding:7px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none;flex-wrap:wrap}
|
|
3092
|
+
.dsh-fk-crumbs{display:flex;align-items:center;gap:1px;min-width:0;flex:1;overflow:hidden;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
3093
|
+
.dsh-fk-crumb{display:inline-flex;align-items:center;gap:4px;max-width:220px;padding:3px 7px;border:0;border-radius:6px;background:transparent;color:inherit;font:inherit;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3094
|
+
.dsh-fk-crumb:hover{background:var(--dsh-fk-hover);color:var(--dsw-alias-label-primary)}
|
|
3095
|
+
.dsh-fk-crumb[data-drop=true]{background:var(--dsh-fk-soft);box-shadow:inset 0 0 0 1px var(--dsh-fk-orange);color:var(--dsh-fk-orange)}
|
|
3096
|
+
.dsh-fk-crumb-current{color:var(--dsw-alias-label-primary);font-weight:500;cursor:default}
|
|
3097
|
+
.dsh-fk-crumb-sep{opacity:.5}
|
|
3098
|
+
.dsh-fk-toolbar-actions{display:flex;align-items:center;gap:4px;flex:none}
|
|
3099
|
+
.dsh-fk-btn{display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer;white-space:nowrap}
|
|
3100
|
+
.dsh-fk-btn:hover{background:var(--dsh-fk-hover)}
|
|
3101
|
+
.dsh-fk-btn:disabled{opacity:.45;cursor:default}
|
|
3102
|
+
.dsh-fk-btn-primary{background:var(--dsh-fk-grad);border-color:transparent;color:#fff;box-shadow:0 1px 6px rgb(244 81 30/.25)}
|
|
3103
|
+
.dsh-fk-btn-primary:hover{filter:brightness(1.05);background:var(--dsh-fk-grad)}
|
|
3104
|
+
.dsh-fk-btn-danger{background:var(--dsw-alias-state-error-primary);border-color:transparent;color:#fff}
|
|
3105
|
+
.dsh-fk-btn-danger:hover{filter:brightness(1.06);background:var(--dsw-alias-state-error-primary)}
|
|
3106
|
+
.dsh-fk-iconbtn{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;flex:none}
|
|
3107
|
+
.dsh-fk-iconbtn:hover{background:var(--dsh-fk-hover);color:var(--dsh-fk-orange)}
|
|
3108
|
+
.dsh-fk-iconbtn:disabled{opacity:.4;cursor:default}
|
|
3109
|
+
.dsh-fk-iconbtn[data-danger=true]:hover{color:var(--dsw-alias-state-error-primary)}
|
|
3110
|
+
.dsh-fk-body{flex:1;min-height:0;overflow:auto;padding:4px 6px 10px}
|
|
3111
|
+
.dsh-fk-row{display:flex;align-items:center;gap:8px;min-height:32px;padding:3px 6px;border-radius:7px;cursor:pointer;outline:none}
|
|
3112
|
+
.dsh-fk-row:hover{background:var(--dsh-fk-hover)}
|
|
3113
|
+
.dsh-fk-row[data-menu=true],.dsh-fk-row[data-selected=true]{background:var(--dsh-fk-active);box-shadow:inset 2px 0 0 var(--dsh-fk-orange)}
|
|
3114
|
+
.dsh-fk-row:focus-visible{outline:1.5px solid var(--dsh-fk-orange);outline-offset:-2px}
|
|
3115
|
+
.dsh-fk-row[data-drop=true]{outline:1.5px dashed var(--dsh-fk-orange);outline-offset:-2px;background:var(--dsh-fk-soft)}
|
|
3116
|
+
.dsh-fk-row-static{cursor:default}
|
|
3117
|
+
.dsh-fk-rowicon{display:flex;color:var(--dsw-alias-label-secondary);flex:none}
|
|
3118
|
+
.dsh-fk-rowicon[data-kind=directory]{color:var(--dsh-fk-dir)}
|
|
3119
|
+
.dsh-fk-rowname{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);flex:0 1 auto}
|
|
3120
|
+
.dsh-fk-rowmain{display:flex;flex-direction:column;min-width:0;flex:1;gap:0}
|
|
3121
|
+
.dsh-fk-rowpath{font-size:11px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3122
|
+
.dsh-fk-rowmeta{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none}
|
|
3123
|
+
.dsh-fk-rowactions{display:flex;align-items:center;gap:2px;flex:none;opacity:0;transition:opacity .12s}
|
|
3124
|
+
.dsh-fk-row:hover .dsh-fk-rowactions,.dsh-fk-row:focus-within .dsh-fk-rowactions{opacity:1}
|
|
3125
|
+
@media (hover:none){.dsh-fk-rowactions{opacity:1}}
|
|
3126
|
+
.dsh-fk-editor{flex:1;min-height:0;display:flex;flex-direction:column;gap:8px;padding:10px 12px 12px}
|
|
3127
|
+
.dsh-fk-codeeditor{position:relative;flex:1;min-height:160px;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base)}
|
|
3128
|
+
.dsh-fk-codeeditor:focus-within{border-color:var(--dsh-fk-orange)}
|
|
3129
|
+
.dsh-fk-codeeditor-pre{position:absolute;inset:0;margin:0;padding:10px 12px;overflow:hidden;pointer-events:none;font:var(--dsh-fk-editor-font)/1.62 var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);white-space:pre;tab-size:2;color:var(--dsw-alias-label-primary)}
|
|
3130
|
+
.dsh-fk-codeeditor-line{min-height:1.62em;white-space:pre}
|
|
3131
|
+
.dsh-fk-codeeditor-input{position:absolute;inset:0;box-sizing:border-box;width:100%;height:100%;margin:0;padding:10px 12px;border:0;resize:none;background:transparent;color:transparent;caret-color:var(--dsw-alias-label-primary);font:var(--dsh-fk-editor-font)/1.62 var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);white-space:pre;overflow:auto;tab-size:2;outline:none}
|
|
3132
|
+
.dsh-fk-codeeditor-input::selection{background:rgb(244 81 30/.24)}
|
|
3133
|
+
.dsh-fk-editor-meta{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary);flex:none}
|
|
3134
|
+
.dsh-fk-badge{display:inline-flex;align-items:center;height:18px;padding:0 7px;margin-left:6px;border-radius:9px;background:var(--dsh-fk-grad);color:#fff;font-size:10px;font-weight:600}
|
|
3135
|
+
.dsh-fk-warn{display:flex;align-items:center;gap:6px;margin:0;padding:7px 10px;border-radius:7px;background:var(--dsh-fk-soft);color:var(--dsw-alias-state-warn-primary);font-size:12px}
|
|
3136
|
+
.dsh-fk-status{padding:7px 14px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px;flex:none;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3137
|
+
.dsh-fk-status[data-kind=error]{color:var(--dsw-alias-state-error-primary)}
|
|
3138
|
+
.dsh-fk-status[data-kind=ok]{color:var(--dsw-alias-state-success-primary)}
|
|
3139
|
+
.dsh-fk-hint{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
3140
|
+
.dsh-fk-center{text-align:center;padding:26px 12px}
|
|
3141
|
+
.dsh-fk-error{margin:0;color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:pre-wrap}
|
|
3142
|
+
.dsh-fk-dialogwrap{position:absolute;inset:0;z-index:30;display:flex;align-items:center;justify-content:center;padding:20px;background:rgb(0 0 0/.32)}
|
|
3143
|
+
.dsh-fk-dialog{display:flex;flex-direction:column;gap:12px;width:min(420px,100%);max-height:calc(100% - 20px);padding:16px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-overlay);box-shadow:0 16px 48px rgb(0 0 0/.30);color:var(--dsw-alias-label-primary);overflow:auto}
|
|
3144
|
+
.dsh-fk-dialog-wide{width:min(560px,100%)}
|
|
3145
|
+
.dsh-fk-dialogtitle{font-size:14px;font-weight:600}
|
|
3146
|
+
.dsh-fk-dialogbody{display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
3147
|
+
.dsh-fk-dialogfoot{display:flex;justify-content:flex-end;gap:8px;flex:none}
|
|
3148
|
+
.dsh-fk-dialogtext{margin:0;font-size:13px;word-break:break-word}
|
|
3149
|
+
.dsh-fk-form{display:flex;flex-direction:column;gap:7px}
|
|
3150
|
+
.dsh-fk-label{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
3151
|
+
.dsh-fk-input{box-sizing:border-box;width:100%;height:32px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;outline:none}
|
|
3152
|
+
.dsh-fk-input:focus{border-color:var(--dsh-fk-orange)}
|
|
3153
|
+
.dsh-fk-dialog-crumbs{flex:none;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:6px}
|
|
3154
|
+
.dsh-fk-picklist{display:flex;flex-direction:column;gap:2px;max-height:260px;overflow:auto;padding:2px}
|
|
3155
|
+
.dsh-fk-pickrow{display:flex;align-items:center;gap:8px;min-height:32px;padding:4px 8px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}
|
|
3156
|
+
.dsh-fk-pickrow:hover{background:var(--dsh-fk-hover)}
|
|
3157
|
+
.dsh-fk-scrim{position:fixed;inset:0;z-index:1300}
|
|
3158
|
+
.dsh-fk-menu{position:fixed;z-index:1301;display:flex;flex-direction:column;gap:1px;padding:5px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-overlay);box-shadow:0 14px 40px rgb(0 0 0/.26)}
|
|
3159
|
+
.dsh-fk-menu button{display:flex;align-items:center;gap:8px;width:100%;padding:6px 9px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3160
|
+
.dsh-fk-menu button:hover{background:var(--dsh-fk-hover)}
|
|
3161
|
+
.dsh-fk-menu button:disabled{opacity:.45;cursor:default}
|
|
3162
|
+
.dsh-fk-menu button[data-danger=true]{color:var(--dsw-alias-state-error-primary)}
|
|
3163
|
+
.dsh-fk-menulabel{overflow:hidden;text-overflow:ellipsis}
|
|
3164
|
+
.dsh-fk-menusep{height:1px;margin:3px 5px;background:var(--dsw-alias-border-l2);flex:none}
|
|
3165
|
+
.dsh-fk-toast{position:fixed;left:50%;bottom:28px;z-index:1500;transform:translateX(-50%);max-width:min(620px,calc(100vw - 32px));padding:8px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-overlay);box-shadow:0 10px 30px rgb(0 0 0/.24);color:var(--dsw-alias-label-primary);font-size:12px;pointer-events:none}
|
|
3166
|
+
.dsh-fk-toast[data-kind=error]{color:var(--dsw-alias-state-error-primary)}
|
|
3167
|
+
.dsh-fk-tabtitle{display:inline-flex;align-items:center;gap:5px;min-width:0}
|
|
3168
|
+
.dsh-fk-tabtitle svg{color:var(--dsh-fk-dir)}
|
|
3169
|
+
.dsh-fk-filestab{background:transparent}
|
|
3170
|
+
.dsh-fk-treehead{display:flex;align-items:center;gap:6px;padding:7px 8px 7px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);flex:none}
|
|
3171
|
+
.dsh-fk-rootpath{display:flex;align-items:center;gap:6px;min-width:0;max-width:100%;padding:4px 7px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}
|
|
3172
|
+
.dsh-fk-rootpath:hover{background:var(--dsh-fk-hover);color:var(--dsw-alias-label-primary)}
|
|
3173
|
+
.dsh-fk-rootpath[data-drop=true]{background:var(--dsh-fk-soft);box-shadow:inset 0 0 0 1px var(--dsh-fk-orange);color:var(--dsh-fk-orange)}
|
|
3174
|
+
.dsh-fk-rootpathtext{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3175
|
+
.dsh-fk-treebody{position:relative;flex:1;min-height:0;overflow:auto;padding:4px 5px 22px}
|
|
3176
|
+
.dsh-fk-tlevel{display:flex;flex-direction:column;gap:0}
|
|
3177
|
+
.dsh-fk-trow{display:flex;align-items:center;gap:5px;min-height:26px;padding:2px 6px 2px 5px;border-radius:6px;cursor:pointer;outline:none;user-select:none}
|
|
3178
|
+
.dsh-fk-trow:hover{background:var(--dsh-fk-hover)}
|
|
3179
|
+
.dsh-fk-trow[data-menu=true],.dsh-fk-trow[data-selected=true]{background:var(--dsh-fk-active);box-shadow:inset 2px 0 0 var(--dsh-fk-orange)}
|
|
3180
|
+
.dsh-fk-trow:focus-visible{outline:1.5px solid var(--dsh-fk-orange);outline-offset:-2px}
|
|
3181
|
+
.dsh-fk-trow[data-drop=true]{outline:1.5px dashed var(--dsh-fk-orange);outline-offset:-2px;background:var(--dsh-fk-soft)}
|
|
3182
|
+
.dsh-fk-trow[data-kind=directory] .dsh-fk-trowname{font-weight:500}
|
|
3183
|
+
.dsh-fk-tchev{display:flex;align-items:center;justify-content:center;width:13px;flex:none;color:var(--dsw-alias-label-tertiary,#8b8b8b)}
|
|
3184
|
+
.dsh-fk-trowicon{display:flex;align-items:center;flex:none;color:var(--dsw-alias-label-secondary)}
|
|
3185
|
+
.dsh-fk-trowicon[data-kind=directory]{color:var(--dsh-fk-dir)}
|
|
3186
|
+
.dsh-fk-trowname{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3187
|
+
.dsh-fk-trowmeta{flex:none;font-size:10.5px;color:var(--dsw-alias-label-secondary);opacity:.85}
|
|
3188
|
+
.dsh-fk-treeline{margin:2px 0 2px 22px;padding:2px 0;font-size:11px}
|
|
3189
|
+
.dsh-fk-treefoot{display:flex;align-items:center;gap:5px;padding:5px 10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:10.5px;color:var(--dsw-alias-label-secondary);flex:none;min-height:24px}
|
|
3190
|
+
.dsh-fk-footnotice{padding:1px 6px;border-radius:5px;background:var(--dsw-alias-bg-layer-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:62%}
|
|
3191
|
+
.dsh-fk-footnotice[data-kind=error]{color:var(--dsw-alias-state-error-primary)}
|
|
3192
|
+
.dsh-fk-footnotice[data-kind=ok]{color:var(--dsw-alias-state-success-primary)}
|
|
3193
|
+
.dsh-fk-rootdrop{position:absolute;left:8px;right:8px;bottom:8px;display:flex;align-items:center;justify-content:center;gap:6px;height:32px;border:1.5px dashed var(--dsh-fk-orange);border-radius:8px;background:var(--dsh-fk-soft);color:var(--dsh-fk-orange);font-size:12px;pointer-events:auto}
|
|
3194
|
+
.dsh-fk-rootdrop[data-over=true]{background:var(--dsh-fk-grad);color:#fff;border-color:transparent}
|
|
3195
|
+
.dsh-fk-dragbar{display:flex;align-items:center;gap:5px;padding:4px 10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:10.5px;color:var(--dsh-fk-orange);flex:none}
|
|
3196
|
+
.dsh-fk-guide{display:flex;flex-direction:column;width:100%;box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-xl,16px);background:var(--dsw-alias-bg-layer-1);overflow:hidden}
|
|
3197
|
+
.dsh-fk-guidecard,.dsh-fk-guideextra{display:flex;align-items:center;gap:12px;width:100%;box-sizing:border-box;padding:12px 16px;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer}
|
|
3198
|
+
.dsh-fk-guidecard{padding-bottom:10px}
|
|
3199
|
+
.dsh-fk-guideextra{padding-top:10px;padding-bottom:11px}
|
|
3200
|
+
.dsh-fk-guidecard:hover,.dsh-fk-guideextra:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))}
|
|
3201
|
+
.dsh-fk-guideicon{display:flex;align-items:center;justify-content:center;width:26px;height:26px;flex:none;color:var(--dsh-fk-dir)}
|
|
3202
|
+
.dsh-fk-guideextraicon{display:flex;align-items:center;justify-content:center;width:26px;height:26px;flex:none;border-radius:7px;background:var(--dsh-fk-grad);color:#fff;box-shadow:0 2px 6px rgb(244 81 30/.25)}
|
|
3203
|
+
.dsh-fk-guidetext{display:flex;flex-direction:column;flex:1;min-width:0;gap:2px}
|
|
3204
|
+
.dsh-fk-guidetitle{font-size:13.5px;line-height:1.35;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3205
|
+
.dsh-fk-guidedesc{font-size:10.5px;line-height:1.35;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3206
|
+
.dsh-fk-guidesep{height:1px;margin:0 14px;background:var(--dsw-alias-border-l3)}
|
|
3207
|
+
.dsh-fk-guidecard>svg,.dsh-fk-guideextra>svg{color:var(--dsw-alias-label-tertiary);flex:none}
|
|
3208
|
+
.dsh-fk-settings{display:flex;flex-direction:column;gap:16px;max-width:680px;padding:4px 0 24px}
|
|
3209
|
+
.dsh-fk-sethead{display:flex;align-items:center;gap:12px;padding-bottom:10px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
3210
|
+
.dsh-fk-setbrand{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:10px;background:var(--dsh-fk-grad);color:#fff;flex:none;box-shadow:0 2px 10px rgb(244 81 30/.28)}
|
|
3211
|
+
.dsh-fk-setheadtext{display:flex;flex-direction:column;gap:2px}
|
|
3212
|
+
.dsh-fk-settitle-main{font-size:15px;font-weight:650}
|
|
3213
|
+
.dsh-fk-setrow{display:flex;align-items:center;gap:18px;justify-content:space-between;padding:10px 2px;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
3214
|
+
.dsh-fk-settext{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
|
|
3215
|
+
.dsh-fk-settitle{font-size:13px;font-weight:550}
|
|
3216
|
+
.dsh-fk-sethint{font-size:11px;line-height:1.45;color:var(--dsw-alias-label-secondary)}
|
|
3217
|
+
.dsh-fk-switch{position:relative;width:38px;height:22px;padding:0;border:0;border-radius:11px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;flex:none;transition:background .15s}
|
|
3218
|
+
.dsh-fk-switch[data-on=true]{background:var(--dsh-fk-grad)}
|
|
3219
|
+
.dsh-fk-switchknob{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgb(0 0 0/.25);transition:left .15s}
|
|
3220
|
+
.dsh-fk-switch[data-on=true] .dsh-fk-switchknob{left:19px}
|
|
3221
|
+
.dsh-fk-segmented{display:inline-flex;gap:2px;padding:2px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex:none}
|
|
3222
|
+
.dsh-fk-segment{padding:4px 9px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11.5px;cursor:pointer;white-space:nowrap}
|
|
3223
|
+
.dsh-fk-segment:hover{background:var(--dsh-fk-hover)}
|
|
3224
|
+
.dsh-fk-segment[data-on=true]{background:var(--dsh-fk-grad);color:#fff}
|
|
3225
|
+
.dsh-fk-fontpicker{display:flex;align-items:center;gap:10px;flex:none}
|
|
3226
|
+
.dsh-fk-range{width:130px;accent-color:var(--dsh-fk-orange)}
|
|
3227
|
+
.dsh-fk-fontvalue{font-size:12px;color:var(--dsw-alias-label-secondary);min-width:34px;text-align:right}
|
|
3228
|
+
.dsh-fk-setfoot{display:flex;align-items:center;gap:10px}
|
|
3229
|
+
.dsh-fk-setflash{font-size:12px;color:var(--dsw-alias-state-success-primary)}
|
|
3230
|
+
@media (max-width:420px){.dsh-fk-setrow{flex-direction:column;align-items:flex-start;gap:8px}.dsh-fk-fontpicker{width:100%}.dsh-fk-range{flex:1}}
|
|
3231
|
+
`;
|
|
3232
|
+
|
|
3233
|
+
// ------------------------------------------------------------------
|
|
3234
|
+
// Plugin body
|
|
3235
|
+
// ------------------------------------------------------------------
|
|
3236
|
+
|
|
3237
|
+
function apply(ctx) {
|
|
3238
|
+
const slots = ctx.get('slots');
|
|
3239
|
+
if (slots === undefined) {
|
|
3240
|
+
console.error('[dsh-filekit] slots service missing; client half disabled');
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
sidebarRightService = ctx.get('sidebarRight');
|
|
3245
|
+
|
|
3246
|
+
ctx.effect(() => {
|
|
3247
|
+
const locale = ctx.get('locale');
|
|
3248
|
+
if (locale === undefined) return undefined;
|
|
3249
|
+
let dispose;
|
|
3250
|
+
try {
|
|
3251
|
+
dispose = locale.register(NS, { zh: DICT.zh, en: DICT.en });
|
|
3252
|
+
translate = locale.bind(NS);
|
|
3253
|
+
} catch (error) {
|
|
3254
|
+
console.warn('[dsh-filekit] locale registration failed:', error?.message ?? error);
|
|
3255
|
+
return undefined;
|
|
3256
|
+
}
|
|
3257
|
+
return () => {
|
|
3258
|
+
translate = fallbackText;
|
|
3259
|
+
try {
|
|
3260
|
+
dispose?.();
|
|
3261
|
+
} catch {
|
|
3262
|
+
/* removing a dictionary must not break unload */
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
}, 'dsh-filekit: dictionaries');
|
|
3266
|
+
|
|
3267
|
+
ctx.effect(() => {
|
|
3268
|
+
const style = createOwnedStyle(CSS);
|
|
3269
|
+
return () => style.remove();
|
|
3270
|
+
}, 'dsh-filekit: styles');
|
|
3271
|
+
|
|
3272
|
+
// Tab types: take over the workspace files page as an extension, and
|
|
3273
|
+
// add the FileKit page as a page type of its own.
|
|
3274
|
+
const tabs = ctx.get('sidebarRightTabs');
|
|
3275
|
+
if (tabs !== undefined && typeof tabs.register === 'function') {
|
|
3276
|
+
ctx.effect(
|
|
3277
|
+
() =>
|
|
3278
|
+
tabs.register({
|
|
3279
|
+
id: FILES_ID,
|
|
3280
|
+
kind: FILES_KIND,
|
|
3281
|
+
priority: 'extension',
|
|
3282
|
+
keepMounted: true,
|
|
3283
|
+
title: () => t('filesTitle'),
|
|
3284
|
+
guide: [
|
|
3285
|
+
{
|
|
3286
|
+
id: 'workspace',
|
|
3287
|
+
order: 10,
|
|
3288
|
+
title: () => t('filesGuideTitle'),
|
|
3289
|
+
description: () => t('filesGuideDescription'),
|
|
3290
|
+
icon: FilesGuideArtwork,
|
|
3291
|
+
},
|
|
3292
|
+
],
|
|
3293
|
+
}),
|
|
3294
|
+
'dsh-filekit: files type',
|
|
3295
|
+
);
|
|
3296
|
+
ctx.effect(
|
|
3297
|
+
() =>
|
|
3298
|
+
tabs.register({
|
|
3299
|
+
id: FILEKIT_ID,
|
|
3300
|
+
kind: FILEKIT_KIND,
|
|
3301
|
+
priority: 'extension',
|
|
3302
|
+
keepMounted: true,
|
|
3303
|
+
title: () => t('title'),
|
|
3304
|
+
}),
|
|
3305
|
+
'dsh-filekit: filekit type',
|
|
3306
|
+
);
|
|
3307
|
+
} else {
|
|
3308
|
+
console.warn('[dsh-filekit] sidebarRightTabs service missing; tabs not registered');
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
if (sidebarRightService !== undefined && typeof sidebarRightService.registerCloseHandler === 'function') {
|
|
3312
|
+
ctx.effect(() => {
|
|
3313
|
+
try {
|
|
3314
|
+
return sidebarRightService.registerCloseHandler(FILEKIT_KIND, (sessionId, tab) => {
|
|
3315
|
+
if (dirtyTabs.has(tab.id)) {
|
|
3316
|
+
showToast(t('saveBeforeClose'), 'error');
|
|
3317
|
+
throw new Error('dsh-filekit: unsaved changes');
|
|
3318
|
+
}
|
|
3319
|
+
});
|
|
3320
|
+
} catch (error) {
|
|
3321
|
+
console.warn('[dsh-filekit] close guard not registered:', error?.message ?? error);
|
|
3322
|
+
return undefined;
|
|
3323
|
+
}
|
|
3324
|
+
}, 'dsh-filekit: close guard');
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
const registerList = (slot, options, Component) => {
|
|
3328
|
+
try {
|
|
3329
|
+
ctx.effect(
|
|
3330
|
+
() => slots.inject(slot, () => slots.register({ name: slot, ...options }, Component)),
|
|
3331
|
+
`dsh-filekit: ${slot}/${options.id ?? ''}`,
|
|
3332
|
+
);
|
|
3333
|
+
} catch (error) {
|
|
3334
|
+
console.warn(`[dsh-filekit] could not register ${slot}:`, error?.message ?? error);
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
|
|
3338
|
+
const registerKeyed = (slot, options, Component) => {
|
|
3339
|
+
try {
|
|
3340
|
+
ctx.effect(
|
|
3341
|
+
() => slots.inject(slot, () => slots.register({ name: slot, ...options }, Component)),
|
|
3342
|
+
`dsh-filekit: ${slot}/${options.key ?? ''}`,
|
|
3343
|
+
);
|
|
3344
|
+
} catch (error) {
|
|
3345
|
+
console.warn(`[dsh-filekit] could not register ${slot}:`, error?.message ?? error);
|
|
3346
|
+
}
|
|
3347
|
+
};
|
|
3348
|
+
|
|
3349
|
+
// The two page bodies and their chip titles.
|
|
3350
|
+
registerKeyed(
|
|
3351
|
+
'sidebar.right.pane.tab',
|
|
3352
|
+
{
|
|
3353
|
+
key: FILES_ID,
|
|
3354
|
+
locale: NS,
|
|
3355
|
+
},
|
|
3356
|
+
FilesBody,
|
|
3357
|
+
);
|
|
3358
|
+
registerKeyed('sidebar.right.pane.tab', { key: FILEKIT_ID, locale: NS }, FileKitTab);
|
|
3359
|
+
registerKeyed('sidebar.right.pane.tab.title', { key: FILES_ID }, FilesTitle);
|
|
3360
|
+
registerKeyed('sidebar.right.pane.tab.title', { key: FILEKIT_ID }, FileKitTitle);
|
|
3361
|
+
|
|
3362
|
+
// The workspace-files guide card gains a FileKit button underneath.
|
|
3363
|
+
registerKeyed('sidebar.right.tab.guide.entry', { key: FILES_ID, locale: NS }, FilesGuideCard);
|
|
3364
|
+
|
|
3365
|
+
// Header actions.
|
|
3366
|
+
registerList('sidebar.right.tab.document.actions', { id: 'filekit-document', order: 20 }, DocumentActions);
|
|
3367
|
+
|
|
3368
|
+
// Settings page.
|
|
3369
|
+
registerList(
|
|
3370
|
+
'settings.section',
|
|
3371
|
+
{ id: 'dsh-filekit', order: 22, label: () => t('settingsTitle') },
|
|
3372
|
+
FileKitSettings,
|
|
3373
|
+
);
|
|
3374
|
+
|
|
3375
|
+
// Transient feedback.
|
|
3376
|
+
registerList('shell.overlay', { id: 'filekit-toast', order: 90 }, ToastHost);
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
return {
|
|
3380
|
+
inject: ['slots'],
|
|
3381
|
+
apply,
|
|
3382
|
+
};
|
|
3383
|
+
},
|
|
3384
|
+
});
|
|
3385
|
+
})();
|