@huyooo/file-explorer-frontend-react 0.4.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.
Files changed (46) hide show
  1. package/dist/index.css +1740 -0
  2. package/dist/index.css.map +1 -0
  3. package/dist/index.d.ts +562 -0
  4. package/dist/index.js +3453 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/style.css +3 -0
  7. package/package.json +58 -0
  8. package/src/components/Breadcrumb.css +61 -0
  9. package/src/components/Breadcrumb.tsx +38 -0
  10. package/src/components/CompressDialog.css +264 -0
  11. package/src/components/CompressDialog.tsx +222 -0
  12. package/src/components/ContextMenu.css +155 -0
  13. package/src/components/ContextMenu.tsx +375 -0
  14. package/src/components/FileGrid.css +267 -0
  15. package/src/components/FileGrid.tsx +277 -0
  16. package/src/components/FileIcon.css +41 -0
  17. package/src/components/FileIcon.tsx +86 -0
  18. package/src/components/FileInfoDialog.css +252 -0
  19. package/src/components/FileInfoDialog.tsx +202 -0
  20. package/src/components/FileList.css +226 -0
  21. package/src/components/FileList.tsx +228 -0
  22. package/src/components/FileListView.css +36 -0
  23. package/src/components/FileListView.tsx +355 -0
  24. package/src/components/FileSidebar.css +94 -0
  25. package/src/components/FileSidebar.tsx +66 -0
  26. package/src/components/ProgressDialog.css +211 -0
  27. package/src/components/ProgressDialog.tsx +183 -0
  28. package/src/components/SortIndicator.css +7 -0
  29. package/src/components/SortIndicator.tsx +19 -0
  30. package/src/components/StatusBar.css +20 -0
  31. package/src/components/StatusBar.tsx +21 -0
  32. package/src/components/Toolbar.css +150 -0
  33. package/src/components/Toolbar.tsx +127 -0
  34. package/src/components/Window.css +246 -0
  35. package/src/components/Window.tsx +335 -0
  36. package/src/hooks/useApplicationIcon.ts +80 -0
  37. package/src/hooks/useDragAndDrop.ts +104 -0
  38. package/src/hooks/useMediaPlayer.ts +164 -0
  39. package/src/hooks/useSelection.ts +112 -0
  40. package/src/hooks/useWindowDrag.ts +60 -0
  41. package/src/hooks/useWindowResize.ts +126 -0
  42. package/src/index.css +3 -0
  43. package/src/index.ts +34 -0
  44. package/src/types/index.ts +274 -0
  45. package/src/utils/fileTypeIcon.ts +309 -0
  46. package/src/utils/folderTypeIcon.ts +132 -0
@@ -0,0 +1,61 @@
1
+ .file-breadcrumb {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 0.125rem;
5
+ overflow-x: auto;
6
+ padding: 0 0.25rem;
7
+ max-width: 100%;
8
+ scrollbar-width: none;
9
+ -ms-overflow-style: none;
10
+ -webkit-app-region: no-drag; /* 面包屑内容不可拖拽,但空白区域可拖拽 */
11
+ }
12
+
13
+ .file-breadcrumb::-webkit-scrollbar {
14
+ display: none;
15
+ }
16
+
17
+ .file-breadcrumb-item {
18
+ display: flex;
19
+ align-items: center;
20
+ flex-shrink: 0;
21
+ }
22
+
23
+ .file-breadcrumb-link {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.375rem;
27
+ padding: 0.25rem 0.5rem;
28
+ border-radius: 0.375rem;
29
+ font-size: 0.875rem;
30
+ transition: all 200ms;
31
+ border: none;
32
+ background: transparent;
33
+ cursor: pointer;
34
+ color: rgb(75, 85, 99);
35
+ overflow: hidden;
36
+ text-overflow: ellipsis;
37
+ white-space: nowrap;
38
+ }
39
+
40
+ .file-breadcrumb-link:hover {
41
+ background: rgba(229, 231, 233, 0.6);
42
+ color: rgb(17, 24, 39);
43
+ }
44
+
45
+ .file-breadcrumb-link--current {
46
+ font-weight: 600;
47
+ color: rgb(17, 24, 39);
48
+ background: rgba(229, 231, 233, 0.5);
49
+ cursor: default;
50
+ }
51
+
52
+ .file-breadcrumb-link--current:hover {
53
+ background: rgba(229, 231, 233, 0.5);
54
+ }
55
+
56
+ .file-breadcrumb-separator {
57
+ color: rgb(156, 163, 175);
58
+ margin: 0 0.25rem;
59
+ flex-shrink: 0;
60
+ }
61
+
@@ -0,0 +1,38 @@
1
+ import { Icon } from '@iconify/react';
2
+ import type { BreadcrumbItem } from '../types';
3
+ import './Breadcrumb.css';
4
+
5
+ interface BreadcrumbProps {
6
+ items: BreadcrumbItem[];
7
+ onNavigate?: (item: BreadcrumbItem, index: number) => void;
8
+ }
9
+
10
+ export function Breadcrumb({ items, onNavigate }: BreadcrumbProps) {
11
+ const handleClick = (item: BreadcrumbItem, index: number) => {
12
+ // 点击最后一项不触发导航
13
+ if (index < items.length - 1) {
14
+ onNavigate?.(item, index);
15
+ }
16
+ };
17
+
18
+ return (
19
+ <div className="file-breadcrumb">
20
+ {items.map((item, index) => (
21
+ <span key={item.id} className="file-breadcrumb-item">
22
+ <span
23
+ onClick={() => handleClick(item, index)}
24
+ className={`file-breadcrumb-link ${
25
+ index === items.length - 1 ? 'file-breadcrumb-link--current' : ''
26
+ }`}
27
+ >
28
+ {item.name}
29
+ </span>
30
+ {index < items.length - 1 && (
31
+ <Icon icon="lucide:chevron-right" width={14} height={14} className="file-breadcrumb-separator" />
32
+ )}
33
+ </span>
34
+ ))}
35
+ </div>
36
+ );
37
+ }
38
+
@@ -0,0 +1,264 @@
1
+ .compress-dialog-overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ background: rgba(0, 0, 0, 0.5);
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ z-index: 10000;
9
+ }
10
+
11
+ .compress-dialog {
12
+ background: white;
13
+ border-radius: 12px;
14
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
15
+ width: 420px;
16
+ max-width: 90vw;
17
+ max-height: 90vh;
18
+ overflow: hidden;
19
+ display: flex;
20
+ flex-direction: column;
21
+ }
22
+
23
+ .compress-dialog-header {
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ padding: 16px 20px;
28
+ border-bottom: 1px solid rgb(229, 231, 233);
29
+ }
30
+
31
+ .compress-dialog-title {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 8px;
35
+ font-weight: 600;
36
+ font-size: 16px;
37
+ color: rgb(17, 24, 39);
38
+ }
39
+
40
+ .compress-dialog-close {
41
+ background: none;
42
+ border: none;
43
+ padding: 4px;
44
+ cursor: pointer;
45
+ color: rgb(107, 114, 128);
46
+ border-radius: 4px;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ }
51
+
52
+ .compress-dialog-close:hover {
53
+ background: rgb(243, 244, 246);
54
+ color: rgb(55, 65, 81);
55
+ }
56
+
57
+ .compress-dialog-content {
58
+ padding: 20px;
59
+ overflow-y: auto;
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 16px;
63
+ }
64
+
65
+ .compress-dialog-info {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 8px;
69
+ padding: 12px;
70
+ background: rgb(249, 250, 251);
71
+ border-radius: 8px;
72
+ color: rgb(55, 65, 81);
73
+ font-size: 14px;
74
+ }
75
+
76
+ .compress-dialog-field {
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 6px;
80
+ }
81
+
82
+ .compress-dialog-field > label {
83
+ font-size: 13px;
84
+ font-weight: 500;
85
+ color: rgb(55, 65, 81);
86
+ }
87
+
88
+ .compress-dialog-input-group {
89
+ display: flex;
90
+ align-items: stretch;
91
+ }
92
+
93
+ .compress-dialog-input-group input {
94
+ flex: 1;
95
+ padding: 8px 12px;
96
+ border: 1px solid rgb(209, 213, 219);
97
+ border-radius: 6px 0 0 6px;
98
+ font-size: 14px;
99
+ outline: none;
100
+ transition: border-color 0.2s;
101
+ }
102
+
103
+ .compress-dialog-input-group input:focus {
104
+ border-color: rgb(59, 130, 246);
105
+ }
106
+
107
+ .compress-dialog-ext {
108
+ padding: 8px 12px;
109
+ background: rgb(243, 244, 246);
110
+ border: 1px solid rgb(209, 213, 219);
111
+ border-left: none;
112
+ border-radius: 0 6px 6px 0;
113
+ font-size: 14px;
114
+ color: rgb(107, 114, 128);
115
+ }
116
+
117
+ .compress-dialog-toggle-password {
118
+ padding: 8px 12px;
119
+ background: rgb(243, 244, 246);
120
+ border: 1px solid rgb(209, 213, 219);
121
+ border-left: none;
122
+ border-radius: 0 6px 6px 0;
123
+ font-size: 12px;
124
+ color: rgb(59, 130, 246);
125
+ cursor: pointer;
126
+ }
127
+
128
+ .compress-dialog-toggle-password:hover {
129
+ background: rgb(229, 231, 235);
130
+ }
131
+
132
+ .compress-dialog-field select {
133
+ padding: 8px 12px;
134
+ border: 1px solid rgb(209, 213, 219);
135
+ border-radius: 6px;
136
+ font-size: 14px;
137
+ outline: none;
138
+ background: white;
139
+ cursor: pointer;
140
+ }
141
+
142
+ .compress-dialog-field select:focus {
143
+ border-color: rgb(59, 130, 246);
144
+ }
145
+
146
+ .compress-dialog-levels {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 8px;
150
+ }
151
+
152
+ .compress-dialog-level {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 8px;
156
+ padding: 8px 12px;
157
+ border: 1px solid rgb(229, 231, 233);
158
+ border-radius: 6px;
159
+ cursor: pointer;
160
+ transition: all 0.2s;
161
+ }
162
+
163
+ .compress-dialog-level:hover {
164
+ background: rgb(249, 250, 251);
165
+ }
166
+
167
+ .compress-dialog-level:has(input:checked) {
168
+ border-color: rgb(59, 130, 246);
169
+ background: rgb(239, 246, 255);
170
+ }
171
+
172
+ .compress-dialog-level input {
173
+ margin: 0;
174
+ }
175
+
176
+ .compress-dialog-level-label {
177
+ font-size: 14px;
178
+ font-weight: 500;
179
+ color: rgb(17, 24, 39);
180
+ }
181
+
182
+ .compress-dialog-level-desc {
183
+ font-size: 12px;
184
+ color: rgb(107, 114, 128);
185
+ margin-left: auto;
186
+ }
187
+
188
+ .compress-dialog-checkbox label {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 8px;
192
+ cursor: pointer;
193
+ font-size: 14px;
194
+ color: rgb(55, 65, 81);
195
+ }
196
+
197
+ .compress-dialog-checkbox input {
198
+ margin: 0;
199
+ width: 16px;
200
+ height: 16px;
201
+ }
202
+
203
+ .compress-dialog-preview {
204
+ padding: 10px 12px;
205
+ background: rgb(249, 250, 251);
206
+ border-radius: 6px;
207
+ font-size: 12px;
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: 4px;
211
+ }
212
+
213
+ .compress-dialog-preview-label {
214
+ color: rgb(107, 114, 128);
215
+ }
216
+
217
+ .compress-dialog-preview-path {
218
+ color: rgb(55, 65, 81);
219
+ word-break: break-all;
220
+ }
221
+
222
+ .compress-dialog-footer {
223
+ display: flex;
224
+ justify-content: flex-end;
225
+ gap: 12px;
226
+ padding: 16px 20px;
227
+ border-top: 1px solid rgb(229, 231, 233);
228
+ }
229
+
230
+ .compress-dialog-btn {
231
+ padding: 8px 20px;
232
+ border-radius: 6px;
233
+ font-size: 14px;
234
+ font-weight: 500;
235
+ cursor: pointer;
236
+ transition: all 0.2s;
237
+ }
238
+
239
+ .compress-dialog-btn-cancel {
240
+ background: white;
241
+ border: 1px solid rgb(209, 213, 219);
242
+ color: rgb(55, 65, 81);
243
+ }
244
+
245
+ .compress-dialog-btn-cancel:hover {
246
+ background: rgb(249, 250, 251);
247
+ }
248
+
249
+ .compress-dialog-btn-confirm {
250
+ background: rgb(59, 130, 246);
251
+ border: 1px solid rgb(59, 130, 246);
252
+ color: white;
253
+ }
254
+
255
+ .compress-dialog-btn-confirm:hover {
256
+ background: rgb(37, 99, 235);
257
+ }
258
+
259
+ .compress-dialog-btn-confirm:disabled {
260
+ background: rgb(156, 163, 175);
261
+ border-color: rgb(156, 163, 175);
262
+ cursor: not-allowed;
263
+ }
264
+
@@ -0,0 +1,222 @@
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { Icon } from '@iconify/react';
4
+ import './CompressDialog.css';
5
+ import type { CompressFormat, CompressLevel, CompressOptions } from '../types';
6
+
7
+ type CompressDialogOptions = Omit<CompressOptions, 'outputDir'>;
8
+
9
+ interface CompressDialogProps {
10
+ visible: boolean;
11
+ /** 要压缩的文件/文件夹路径列表 */
12
+ filePaths: string[];
13
+ /** 输出目录 */
14
+ outputDir: string;
15
+ onConfirm: (options: CompressDialogOptions) => void;
16
+ onCancel: () => void;
17
+ }
18
+
19
+ /** 格式选项配置 */
20
+ const FORMAT_OPTIONS: { value: CompressFormat; label: string; ext: string }[] = [
21
+ { value: 'zip', label: 'ZIP', ext: '.zip' },
22
+ { value: 'tgz', label: 'TAR.GZ (gzip)', ext: '.tar.gz' },
23
+ { value: 'tarbz2', label: 'TAR.BZ2 (bzip2)', ext: '.tar.bz2' },
24
+ { value: 'tar', label: 'TAR (无压缩)', ext: '.tar' },
25
+ ];
26
+
27
+ /** 压缩级别选项 */
28
+ const LEVEL_OPTIONS: { value: CompressLevel; label: string; desc: string }[] = [
29
+ { value: 'fast', label: '快速', desc: '压缩速度快,文件较大' },
30
+ { value: 'normal', label: '标准', desc: '平衡速度和大小' },
31
+ { value: 'best', label: '最佳', desc: '文件最小,速度较慢' },
32
+ ];
33
+
34
+ /**
35
+ * 压缩对话框组件
36
+ */
37
+ export function CompressDialog({
38
+ visible,
39
+ filePaths,
40
+ outputDir,
41
+ onConfirm,
42
+ onCancel,
43
+ }: CompressDialogProps) {
44
+ const [format, setFormat] = useState<CompressFormat>('zip');
45
+ const [level, setLevel] = useState<CompressLevel>('normal');
46
+ const [outputName, setOutputName] = useState('');
47
+ const [deleteSource, setDeleteSource] = useState(false);
48
+ const [password, setPassword] = useState('');
49
+ const [showPassword, setShowPassword] = useState(false);
50
+
51
+ // 根据选中文件生成默认输出名称
52
+ const defaultOutputName = useMemo(() => {
53
+ if (filePaths.length === 0) return 'archive';
54
+ if (filePaths.length === 1) {
55
+ const name = filePaths[0].split('/').pop() || 'archive';
56
+ // 移除扩展名
57
+ return name.replace(/\.[^.]+$/, '');
58
+ }
59
+ return '压缩文件';
60
+ }, [filePaths]);
61
+
62
+ // 初始化输出名称
63
+ useEffect(() => {
64
+ if (visible) {
65
+ setOutputName(defaultOutputName);
66
+ setFormat('zip');
67
+ setLevel('normal');
68
+ setDeleteSource(false);
69
+ setPassword('');
70
+ }
71
+ }, [visible, defaultOutputName]);
72
+
73
+ // 获取当前格式的扩展名
74
+ const currentExt = FORMAT_OPTIONS.find(f => f.value === format)?.ext || '.zip';
75
+
76
+ // 完整输出路径预览
77
+ const fullOutputPath = `${outputDir}/${outputName}${currentExt}`;
78
+
79
+ const handleConfirm = () => {
80
+ onConfirm({
81
+ format,
82
+ level,
83
+ outputName: outputName + currentExt,
84
+ deleteSource,
85
+ });
86
+ };
87
+
88
+ // 当前后端未实现密码压缩,避免 UI 误导:直接关闭密码选项
89
+ const supportsPassword = false;
90
+
91
+ if (!visible) return null;
92
+
93
+ return createPortal(
94
+ <div className="compress-dialog-overlay" onClick={onCancel}>
95
+ <div className="compress-dialog" onClick={e => e.stopPropagation()}>
96
+ {/* 头部 */}
97
+ <div className="compress-dialog-header">
98
+ <div className="compress-dialog-title">
99
+ <Icon icon="lucide:archive" width={20} height={20} />
100
+ <span>压缩文件</span>
101
+ </div>
102
+ <button className="compress-dialog-close" onClick={onCancel}>
103
+ <Icon icon="lucide:x" width={18} height={18} />
104
+ </button>
105
+ </div>
106
+
107
+ {/* 内容 */}
108
+ <div className="compress-dialog-content">
109
+ {/* 文件信息 */}
110
+ <div className="compress-dialog-info">
111
+ <Icon icon="lucide:file-archive" width={16} height={16} />
112
+ <span>
113
+ {filePaths.length === 1
114
+ ? filePaths[0].split('/').pop()
115
+ : `${filePaths.length} 个项目`}
116
+ </span>
117
+ </div>
118
+
119
+ {/* 输出文件名 */}
120
+ <div className="compress-dialog-field">
121
+ <label>文件名</label>
122
+ <div className="compress-dialog-input-group">
123
+ <input
124
+ type="text"
125
+ value={outputName}
126
+ onChange={e => setOutputName(e.target.value)}
127
+ placeholder="输入文件名"
128
+ />
129
+ <span className="compress-dialog-ext">{currentExt}</span>
130
+ </div>
131
+ </div>
132
+
133
+ {/* 压缩格式 */}
134
+ <div className="compress-dialog-field">
135
+ <label>压缩格式</label>
136
+ <select value={format} onChange={e => setFormat(e.target.value as CompressFormat)}>
137
+ {FORMAT_OPTIONS.map(opt => (
138
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
139
+ ))}
140
+ </select>
141
+ </div>
142
+
143
+ {/* 压缩级别 */}
144
+ <div className="compress-dialog-field">
145
+ <label>压缩级别</label>
146
+ <div className="compress-dialog-levels">
147
+ {LEVEL_OPTIONS.map(opt => (
148
+ <label key={opt.value} className="compress-dialog-level">
149
+ <input
150
+ type="radio"
151
+ name="level"
152
+ value={opt.value}
153
+ checked={level === opt.value}
154
+ onChange={() => setLevel(opt.value)}
155
+ />
156
+ <span className="compress-dialog-level-label">{opt.label}</span>
157
+ <span className="compress-dialog-level-desc">{opt.desc}</span>
158
+ </label>
159
+ ))}
160
+ </div>
161
+ </div>
162
+
163
+ {/* 密码保护(仅 zip/7z) */}
164
+ {supportsPassword && (
165
+ <div className="compress-dialog-field">
166
+ <label>密码保护(可选)</label>
167
+ <div className="compress-dialog-input-group">
168
+ <input
169
+ type={showPassword ? 'text' : 'password'}
170
+ value={password}
171
+ onChange={e => setPassword(e.target.value)}
172
+ placeholder="设置密码"
173
+ />
174
+ <button
175
+ type="button"
176
+ className="compress-dialog-toggle-password"
177
+ onClick={() => setShowPassword(!showPassword)}
178
+ >
179
+ {showPassword ? '隐藏' : '显示'}
180
+ </button>
181
+ </div>
182
+ </div>
183
+ )}
184
+
185
+ {/* 删除源文件选项 */}
186
+ <div className="compress-dialog-field compress-dialog-checkbox">
187
+ <label>
188
+ <input
189
+ type="checkbox"
190
+ checked={deleteSource}
191
+ onChange={e => setDeleteSource(e.target.checked)}
192
+ />
193
+ <span>压缩后删除源文件</span>
194
+ </label>
195
+ </div>
196
+
197
+ {/* 输出路径预览 */}
198
+ <div className="compress-dialog-preview">
199
+ <span className="compress-dialog-preview-label">输出位置:</span>
200
+ <span className="compress-dialog-preview-path">{fullOutputPath}</span>
201
+ </div>
202
+ </div>
203
+
204
+ {/* 底部按钮 */}
205
+ <div className="compress-dialog-footer">
206
+ <button className="compress-dialog-btn compress-dialog-btn-cancel" onClick={onCancel}>
207
+ 取消
208
+ </button>
209
+ <button
210
+ className="compress-dialog-btn compress-dialog-btn-confirm"
211
+ onClick={handleConfirm}
212
+ disabled={!outputName.trim()}
213
+ >
214
+ 压缩
215
+ </button>
216
+ </div>
217
+ </div>
218
+ </div>,
219
+ document.body
220
+ );
221
+ }
222
+