@huyooo/file-explorer-frontend-react 0.4.18 → 0.4.21

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 (44) hide show
  1. package/dist/index.css +0 -1
  2. package/dist/index.js +1 -3456
  3. package/package.json +4 -4
  4. package/dist/index.css.map +0 -1
  5. package/dist/index.js.map +0 -1
  6. package/src/components/Breadcrumb.css +0 -61
  7. package/src/components/Breadcrumb.tsx +0 -38
  8. package/src/components/CompressDialog.css +0 -267
  9. package/src/components/CompressDialog.tsx +0 -222
  10. package/src/components/ContextMenu.css +0 -155
  11. package/src/components/ContextMenu.tsx +0 -375
  12. package/src/components/FileGrid.css +0 -239
  13. package/src/components/FileGrid.tsx +0 -278
  14. package/src/components/FileIcon.css +0 -41
  15. package/src/components/FileIcon.tsx +0 -86
  16. package/src/components/FileInfoDialog.css +0 -214
  17. package/src/components/FileInfoDialog.tsx +0 -202
  18. package/src/components/FileList.css +0 -169
  19. package/src/components/FileList.tsx +0 -228
  20. package/src/components/FileListView.css +0 -36
  21. package/src/components/FileListView.tsx +0 -355
  22. package/src/components/FileSidebar.css +0 -94
  23. package/src/components/FileSidebar.tsx +0 -66
  24. package/src/components/ProgressDialog.css +0 -211
  25. package/src/components/ProgressDialog.tsx +0 -183
  26. package/src/components/SortIndicator.css +0 -7
  27. package/src/components/SortIndicator.tsx +0 -19
  28. package/src/components/StatusBar.css +0 -20
  29. package/src/components/StatusBar.tsx +0 -21
  30. package/src/components/Toolbar.css +0 -150
  31. package/src/components/Toolbar.tsx +0 -127
  32. package/src/components/Window.css +0 -246
  33. package/src/components/Window.tsx +0 -335
  34. package/src/hooks/useApplicationIcon.ts +0 -80
  35. package/src/hooks/useDragAndDrop.ts +0 -104
  36. package/src/hooks/useMediaPlayer.ts +0 -164
  37. package/src/hooks/useSelection.ts +0 -112
  38. package/src/hooks/useWindowDrag.ts +0 -60
  39. package/src/hooks/useWindowResize.ts +0 -126
  40. package/src/index.css +0 -184
  41. package/src/index.ts +0 -37
  42. package/src/types/index.ts +0 -274
  43. package/src/utils/fileTypeIcon.ts +0 -309
  44. package/src/utils/folderTypeIcon.ts +0 -132
@@ -1,38 +0,0 @@
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
-
@@ -1,267 +0,0 @@
1
- .compress-dialog-overlay {
2
- position: fixed;
3
- inset: 0;
4
- background: var(--huyooo-overlay);
5
- display: flex;
6
- align-items: center;
7
- justify-content: center;
8
- z-index: 10000;
9
- }
10
-
11
- .compress-dialog {
12
- background: var(--huyooo-panel-bg);
13
- border-radius: 12px;
14
- box-shadow: var(--huyooo-shadow-lg);
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 var(--huyooo-border);
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: var(--huyooo-text);
38
- }
39
-
40
- .compress-dialog-close {
41
- background: none;
42
- border: none;
43
- padding: 4px;
44
- cursor: pointer;
45
- color: var(--huyooo-text-muted);
46
- border-radius: 4px;
47
- display: flex;
48
- align-items: center;
49
- justify-content: center;
50
- }
51
-
52
- .compress-dialog-close:hover {
53
- background: var(--huyooo-muted);
54
- color: var(--huyooo-text);
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: var(--huyooo-surface);
71
- border-radius: 8px;
72
- color: var(--huyooo-text);
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: var(--huyooo-text);
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 var(--huyooo-border);
97
- border-radius: 6px 0 0 6px;
98
- font-size: 14px;
99
- outline: none;
100
- transition: border-color 0.2s;
101
- background: var(--huyooo-surface-2);
102
- color: var(--huyooo-text);
103
- }
104
-
105
- .compress-dialog-input-group input:focus {
106
- border-color: var(--huyooo-primary);
107
- }
108
-
109
- .compress-dialog-ext {
110
- padding: 8px 12px;
111
- background: var(--huyooo-muted);
112
- border: 1px solid var(--huyooo-border);
113
- border-left: none;
114
- border-radius: 0 6px 6px 0;
115
- font-size: 14px;
116
- color: var(--huyooo-text-muted);
117
- }
118
-
119
- .compress-dialog-toggle-password {
120
- padding: 8px 12px;
121
- background: var(--huyooo-muted);
122
- border: 1px solid var(--huyooo-border);
123
- border-left: none;
124
- border-radius: 0 6px 6px 0;
125
- font-size: 12px;
126
- color: var(--huyooo-primary);
127
- cursor: pointer;
128
- }
129
-
130
- .compress-dialog-toggle-password:hover {
131
- background: var(--huyooo-muted-hover);
132
- }
133
-
134
- .compress-dialog-field select {
135
- padding: 8px 12px;
136
- border: 1px solid var(--huyooo-border);
137
- border-radius: 6px;
138
- font-size: 14px;
139
- outline: none;
140
- background: var(--huyooo-surface-2);
141
- color: var(--huyooo-text);
142
- cursor: pointer;
143
- }
144
-
145
- .compress-dialog-field select:focus {
146
- border-color: var(--huyooo-primary);
147
- }
148
-
149
- .compress-dialog-levels {
150
- display: flex;
151
- flex-direction: column;
152
- gap: 8px;
153
- }
154
-
155
- .compress-dialog-level {
156
- display: flex;
157
- align-items: center;
158
- gap: 8px;
159
- padding: 8px 12px;
160
- border: 1px solid var(--huyooo-border);
161
- border-radius: 6px;
162
- cursor: pointer;
163
- transition: all 0.2s;
164
- }
165
-
166
- .compress-dialog-level:hover {
167
- background: var(--huyooo-surface);
168
- }
169
-
170
- .compress-dialog-level:has(input:checked) {
171
- border-color: var(--huyooo-primary);
172
- background: var(--huyooo-focus-ring);
173
- }
174
-
175
- .compress-dialog-level input {
176
- margin: 0;
177
- }
178
-
179
- .compress-dialog-level-label {
180
- font-size: 14px;
181
- font-weight: 500;
182
- color: var(--huyooo-text);
183
- }
184
-
185
- .compress-dialog-level-desc {
186
- font-size: 12px;
187
- color: var(--huyooo-text-muted);
188
- margin-left: auto;
189
- }
190
-
191
- .compress-dialog-checkbox label {
192
- display: flex;
193
- align-items: center;
194
- gap: 8px;
195
- cursor: pointer;
196
- font-size: 14px;
197
- color: var(--huyooo-text);
198
- }
199
-
200
- .compress-dialog-checkbox input {
201
- margin: 0;
202
- width: 16px;
203
- height: 16px;
204
- }
205
-
206
- .compress-dialog-preview {
207
- padding: 10px 12px;
208
- background: var(--huyooo-surface);
209
- border-radius: 6px;
210
- font-size: 12px;
211
- display: flex;
212
- flex-direction: column;
213
- gap: 4px;
214
- }
215
-
216
- .compress-dialog-preview-label {
217
- color: var(--huyooo-text-muted);
218
- }
219
-
220
- .compress-dialog-preview-path {
221
- color: var(--huyooo-text);
222
- word-break: break-all;
223
- }
224
-
225
- .compress-dialog-footer {
226
- display: flex;
227
- justify-content: flex-end;
228
- gap: 12px;
229
- padding: 16px 20px;
230
- border-top: 1px solid var(--huyooo-border);
231
- }
232
-
233
- .compress-dialog-btn {
234
- padding: 8px 20px;
235
- border-radius: 6px;
236
- font-size: 14px;
237
- font-weight: 500;
238
- cursor: pointer;
239
- transition: all 0.2s;
240
- }
241
-
242
- .compress-dialog-btn-cancel {
243
- background: var(--huyooo-surface-2);
244
- border: 1px solid var(--huyooo-border);
245
- color: var(--huyooo-text);
246
- }
247
-
248
- .compress-dialog-btn-cancel:hover {
249
- background: var(--huyooo-surface);
250
- }
251
-
252
- .compress-dialog-btn-confirm {
253
- background: var(--huyooo-primary);
254
- border: 1px solid var(--huyooo-primary);
255
- color: white;
256
- }
257
-
258
- .compress-dialog-btn-confirm:hover {
259
- background: var(--huyooo-primary-hover, var(--huyooo-primary));
260
- }
261
-
262
- .compress-dialog-btn-confirm:disabled {
263
- background: var(--huyooo-text-disabled);
264
- border-color: var(--huyooo-text-disabled);
265
- cursor: not-allowed;
266
- }
267
-
@@ -1,222 +0,0 @@
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
-
@@ -1,155 +0,0 @@
1
- /* container 不拦截事件,让右键事件可以穿透到下层文件列表 */
2
- .context-menu-container {
3
- position: fixed;
4
- inset: 0;
5
- z-index: 9999;
6
- pointer-events: none;
7
- }
8
-
9
- .context-menu {
10
- position: fixed;
11
- z-index: 10000;
12
- width: 220px; /* 固定宽度,与 calculateMenuPosition 中的 MENU_WIDTH 一致 */
13
- background: var(--huyooo-menu-bg);
14
- backdrop-filter: blur(24px);
15
- border: 1px solid var(--huyooo-border);
16
- border-radius: 0.5rem;
17
- box-shadow: var(--huyooo-menu-shadow);
18
- padding: 0.25rem 0;
19
- font-size: 0.875rem;
20
- user-select: none;
21
- animation: fade-in 100ms ease-out, zoom-in 100ms ease-out;
22
- pointer-events: auto;
23
- }
24
-
25
- @keyframes fade-in {
26
- from { opacity: 0; }
27
- to { opacity: 1; }
28
- }
29
-
30
- @keyframes zoom-in {
31
- from { transform: scale(0.95); }
32
- to { transform: scale(1); }
33
- }
34
-
35
- .context-menu-item {
36
- width: 100%;
37
- text-align: left;
38
- display: flex;
39
- align-items: center;
40
- gap: 0.5rem;
41
- padding: 0.375rem 1rem;
42
- color: var(--huyooo-text);
43
- transition: all 200ms;
44
- border: none;
45
- background: transparent;
46
- cursor: pointer;
47
- }
48
-
49
- .context-menu-item:hover {
50
- background: var(--huyooo-primary);
51
- color: white;
52
- }
53
-
54
- .context-menu-item:hover .context-menu-item-icon {
55
- color: white;
56
- }
57
-
58
- .context-menu-item:hover .context-menu-item-shortcut {
59
- color: var(--huyooo-on-primary-muted);
60
- }
61
-
62
- .context-menu-item--disabled {
63
- opacity: 0.5;
64
- cursor: not-allowed;
65
- }
66
-
67
- .context-menu-item--disabled:hover {
68
- background: transparent;
69
- color: var(--huyooo-text);
70
- }
71
-
72
- .context-menu-item--disabled:hover .context-menu-item-icon {
73
- color: var(--huyooo-text-muted);
74
- }
75
-
76
- .context-menu-item--danger {
77
- color: var(--huyooo-danger);
78
- }
79
-
80
- .context-menu-item--danger:hover {
81
- background: var(--huyooo-danger);
82
- color: white;
83
- }
84
-
85
- .context-menu-item--danger .context-menu-item-icon {
86
- color: var(--huyooo-danger);
87
- }
88
-
89
- .context-menu-item--danger:hover .context-menu-item-icon {
90
- color: white;
91
- }
92
-
93
- .context-menu-item-icon {
94
- color: var(--huyooo-text-muted);
95
- flex-shrink: 0;
96
- }
97
-
98
- .context-menu-item-label {
99
- flex: 1;
100
- }
101
-
102
- .context-menu-item-shortcut {
103
- color: var(--huyooo-text-disabled);
104
- font-size: 0.6875rem;
105
- }
106
-
107
- .context-menu-separator {
108
- height: 1px;
109
- background: var(--huyooo-border);
110
- margin: 0.25rem 0.5rem;
111
- }
112
-
113
- .context-menu-item--has-children {
114
- position: relative;
115
- }
116
-
117
- .context-menu-item--active {
118
- background: var(--huyooo-primary);
119
- color: white;
120
- }
121
-
122
- .context-menu-item--active .context-menu-item-icon {
123
- color: white;
124
- }
125
-
126
- .context-menu-item--active .context-menu-item-shortcut {
127
- color: var(--huyooo-on-primary-muted);
128
- }
129
-
130
- .context-menu-item-check {
131
- color: var(--huyooo-text-muted);
132
- flex-shrink: 0;
133
- margin-left: auto;
134
- }
135
-
136
- .context-menu-item:hover .context-menu-item-check,
137
- .context-menu-item--active .context-menu-item-check {
138
- color: white;
139
- }
140
-
141
- .context-menu-item-arrow {
142
- color: var(--huyooo-text-disabled);
143
- flex-shrink: 0;
144
- margin-left: auto;
145
- }
146
-
147
- .context-menu-item:hover .context-menu-item-arrow,
148
- .context-menu-item--active .context-menu-item-arrow {
149
- color: white;
150
- }
151
-
152
- .context-menu-submenu {
153
- z-index: 10001;
154
- }
155
-