@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,375 +0,0 @@
1
- import { createPortal } from 'react-dom';
2
- import { useMemo, useEffect, useRef, useCallback, useState } from 'react';
3
- import { Icon } from '@iconify/react';
4
- import type { ContextMenuItem } from '../types';
5
- import './ContextMenu.css';
6
-
7
- interface ContextMenuProps {
8
- visible: boolean;
9
- x: number;
10
- y: number;
11
- options: ContextMenuItem[];
12
- onClose?: () => void;
13
- onSelect?: (option: ContextMenuItem) => void;
14
- }
15
-
16
- /** 边距常量 */
17
- const MARGIN = 8;
18
- /** 菜单固定宽度 */
19
- const MENU_WIDTH = 220;
20
- /** 菜单项高度 */
21
- const MENU_ITEM_HEIGHT = 32;
22
- /** 分隔线高度 */
23
- const SEPARATOR_HEIGHT = 9;
24
- /** 菜单内边距 */
25
- const MENU_PADDING = 8;
26
- /** 子菜单与父菜单的间距 */
27
- const SUBMENU_GAP = 0;
28
-
29
- /**
30
- * 估算菜单高度(根据菜单项数量)
31
- */
32
- function estimateMenuHeight(items: ContextMenuItem[]): number {
33
- let height = MENU_PADDING;
34
- for (const item of items) {
35
- height += item.separator ? SEPARATOR_HEIGHT : MENU_ITEM_HEIGHT;
36
- }
37
- return height;
38
- }
39
-
40
- /**
41
- * 计算自适应菜单位置
42
- * 使用固定宽度,只需测量高度,简化计算逻辑
43
- */
44
- function calculateMenuPosition(
45
- clickX: number,
46
- clickY: number,
47
- menuHeight: number
48
- ): { x: number; y: number } {
49
- const viewportWidth = window.innerWidth;
50
- const viewportHeight = window.innerHeight;
51
-
52
- let adjustedX = clickX;
53
- let adjustedY = clickY;
54
-
55
- // 水平方向调整(使用固定宽度)
56
- const spaceRight = viewportWidth - clickX;
57
- const spaceLeft = clickX;
58
-
59
- // 如果右边空间不足,尝试在左边显示
60
- if (spaceRight < MENU_WIDTH + MARGIN) {
61
- // 左边空间足够,在左边显示
62
- if (spaceLeft >= MENU_WIDTH + MARGIN) {
63
- adjustedX = clickX - MENU_WIDTH;
64
- } else {
65
- // 左右空间都不足,选择空间更大的一边,并留出边距
66
- if (spaceRight > spaceLeft) {
67
- adjustedX = viewportWidth - MENU_WIDTH - MARGIN;
68
- } else {
69
- adjustedX = MARGIN;
70
- }
71
- }
72
- }
73
-
74
- // 垂直方向调整
75
- const spaceBottom = viewportHeight - clickY;
76
- const spaceTop = clickY;
77
-
78
- // 如果下边空间不足,尝试在上边显示
79
- if (spaceBottom < menuHeight + MARGIN) {
80
- // 上边空间足够,在上边显示
81
- if (spaceTop >= menuHeight + MARGIN) {
82
- adjustedY = clickY - menuHeight;
83
- } else {
84
- // 上下空间都不足,选择空间更大的一边,并留出边距
85
- if (spaceBottom > spaceTop) {
86
- adjustedY = viewportHeight - menuHeight - MARGIN;
87
- } else {
88
- adjustedY = MARGIN;
89
- }
90
- }
91
- }
92
-
93
- // 最终边界检查,确保不会超出视口
94
- adjustedX = Math.max(MARGIN, Math.min(adjustedX, viewportWidth - MENU_WIDTH - MARGIN));
95
- adjustedY = Math.max(MARGIN, Math.min(adjustedY, viewportHeight - menuHeight - MARGIN));
96
-
97
- return { x: adjustedX, y: adjustedY };
98
- }
99
-
100
- /**
101
- * 计算二级菜单位置(Windows/Mac 标准行为:优先右侧,空间不足时左侧)
102
- * @param parentRect 父菜单项的边界矩形
103
- * @param submenuHeight 子菜单高度
104
- * @returns 子菜单位置
105
- */
106
- function calculateSubmenuPosition(
107
- parentRect: DOMRect,
108
- submenuHeight: number
109
- ): { x: number; y: number } {
110
- const viewportWidth = window.innerWidth;
111
- const viewportHeight = window.innerHeight;
112
-
113
- // 计算可用空间
114
- const spaceRight = viewportWidth - parentRect.right;
115
- const spaceLeft = parentRect.left;
116
-
117
- let submenuX: number;
118
- let submenuY: number;
119
-
120
- // 水平方向:优先右侧,空间不足时左侧(Windows/Mac 标准行为)
121
- if (spaceRight >= MENU_WIDTH + SUBMENU_GAP + MARGIN) {
122
- // 右侧有足够空间,在右侧显示(紧贴父菜单右边缘)
123
- submenuX = parentRect.right + SUBMENU_GAP;
124
- } else if (spaceLeft >= MENU_WIDTH + SUBMENU_GAP + MARGIN) {
125
- // 右侧空间不足,左侧有足够空间,在左侧显示(紧贴父菜单左边缘)
126
- submenuX = parentRect.left - MENU_WIDTH - SUBMENU_GAP;
127
- } else {
128
- // 左右空间都不足,选择空间更大的一边
129
- if (spaceRight > spaceLeft) {
130
- submenuX = viewportWidth - MENU_WIDTH - MARGIN;
131
- } else {
132
- submenuX = MARGIN;
133
- }
134
- }
135
-
136
- // 垂直方向:与父菜单项顶部对齐(Windows/Mac 标准行为)
137
- submenuY = parentRect.top;
138
-
139
- // 如果子菜单底部超出视口,向上调整
140
- if (submenuY + submenuHeight > viewportHeight - MARGIN) {
141
- submenuY = viewportHeight - submenuHeight - MARGIN;
142
- }
143
-
144
- // 如果子菜单顶部超出视口,向下调整
145
- if (submenuY < MARGIN) {
146
- submenuY = MARGIN;
147
- }
148
-
149
- return { x: submenuX, y: submenuY };
150
- }
151
-
152
- export function ContextMenu({
153
- visible,
154
- x,
155
- y,
156
- options,
157
- onClose,
158
- onSelect,
159
- }: ContextMenuProps) {
160
- const menuRef = useRef<HTMLDivElement>(null);
161
- const [hoveredItemId, setHoveredItemId] = useState<string | null>(null);
162
- const [submenuPosition, setSubmenuPosition] = useState<{ x: number; y: number } | null>(null);
163
- const itemRefs = useRef<Map<string, HTMLDivElement>>(new Map());
164
-
165
- // 使用 useMemo 在渲染前计算位置(使用估算高度,避免位置调整动画)
166
- const position = useMemo(() => {
167
- const estimatedHeight = estimateMenuHeight(options);
168
- return calculateMenuPosition(x, y, estimatedHeight);
169
- }, [x, y, options]);
170
-
171
- const menuStyle = useMemo(
172
- () => ({
173
- left: `${position.x}px`,
174
- top: `${position.y}px`,
175
- }),
176
- [position]
177
- );
178
-
179
- // 获取当前悬停项的子菜单
180
- const hoveredItem = useMemo(() => {
181
- if (!hoveredItemId) return null;
182
- return options.find(opt => opt.id === hoveredItemId && opt.children && opt.children.length > 0);
183
- }, [hoveredItemId, options]);
184
-
185
- // 计算子菜单位置
186
- useEffect(() => {
187
- if (!hoveredItemId || !hoveredItem) {
188
- setSubmenuPosition(null);
189
- return;
190
- }
191
-
192
- const itemEl = itemRefs.current.get(hoveredItemId);
193
- if (!itemEl) {
194
- setSubmenuPosition(null);
195
- return;
196
- }
197
-
198
- const rect = itemEl.getBoundingClientRect();
199
- const submenuHeight = estimateMenuHeight(hoveredItem.children || []);
200
- const pos = calculateSubmenuPosition(rect, submenuHeight);
201
- setSubmenuPosition(pos);
202
- }, [hoveredItemId, hoveredItem]);
203
-
204
- // 点击外部或按 ESC 键时关闭
205
- useEffect(() => {
206
- if (!visible) return;
207
-
208
- const handleClickOutside = (e: MouseEvent) => {
209
- const target = e.target as Node;
210
-
211
- // 检查是否点击在菜单内(包括主菜单和子菜单)
212
- const menuContainer = document.querySelector('.context-menu-container');
213
- if (menuContainer && menuContainer.contains(target)) {
214
- return;
215
- }
216
-
217
- onClose?.();
218
- };
219
-
220
- const handleEscape = (e: KeyboardEvent) => {
221
- if (e.key === 'Escape') {
222
- onClose?.();
223
- }
224
- };
225
-
226
- // 使用 mousedown 来更快响应(Windows/Mac 标准行为)
227
- document.addEventListener('mousedown', handleClickOutside);
228
- document.addEventListener('keydown', handleEscape);
229
-
230
- return () => {
231
- document.removeEventListener('mousedown', handleClickOutside);
232
- document.removeEventListener('keydown', handleEscape);
233
- };
234
- }, [visible, onClose]);
235
-
236
- // 重置状态当菜单关闭时
237
- useEffect(() => {
238
- if (!visible) {
239
- setHoveredItemId(null);
240
- setSubmenuPosition(null);
241
- }
242
- }, [visible]);
243
-
244
- // Iconify 图标渲染函数
245
- const renderIcon = (iconName: string) => {
246
- if (!iconName) return null;
247
- return <Icon icon={iconName} width={16} height={16} className="context-menu-item-icon" />;
248
- };
249
-
250
- const handleOptionClick = useCallback(
251
- (option: ContextMenuItem) => {
252
- if (option.disabled) return;
253
-
254
- // 如果有子菜单,不执行 action,只显示子菜单
255
- if (option.children && option.children.length > 0) {
256
- return;
257
- }
258
-
259
- if (option.action) {
260
- option.action();
261
- }
262
- onSelect?.(option);
263
- onClose?.();
264
- },
265
- [onClose, onSelect]
266
- );
267
-
268
- // 处理菜单项鼠标进入
269
- const handleItemMouseEnter = useCallback((option: ContextMenuItem) => {
270
- if (option.children && option.children.length > 0) {
271
- setHoveredItemId(option.id);
272
- } else {
273
- setHoveredItemId(null);
274
- }
275
- }, []);
276
-
277
- if (!visible) return null;
278
-
279
- return createPortal(
280
- <div className="context-menu-container">
281
- {/* 主菜单 - 不使用 overlay,通过 document mousedown 事件来关闭菜单 */}
282
- <div ref={menuRef} className="context-menu" style={menuStyle}>
283
- {options.map((option, index) => {
284
- if (option.separator) {
285
- return <div key={index} className="context-menu-separator" />;
286
- }
287
-
288
- const hasChildren = option.children && option.children.length > 0;
289
- const isHovered = hoveredItemId === option.id;
290
-
291
- return (
292
- <div
293
- key={option.id || index}
294
- ref={(el) => {
295
- if (el) {
296
- itemRefs.current.set(option.id, el);
297
- } else {
298
- itemRefs.current.delete(option.id);
299
- }
300
- }}
301
- className={`context-menu-item ${
302
- option.disabled ? 'context-menu-item--disabled' : ''
303
- } ${
304
- option.danger ? 'context-menu-item--danger' : ''
305
- } ${
306
- hasChildren ? 'context-menu-item--has-children' : ''
307
- } ${
308
- isHovered ? 'context-menu-item--active' : ''
309
- }`}
310
- onClick={() => handleOptionClick(option)}
311
- onMouseEnter={() => handleItemMouseEnter(option)}
312
- >
313
- {option.icon && renderIcon(option.icon)}
314
- <span className="context-menu-item-label">{option.label}</span>
315
- {option.checked && (
316
- <Icon icon="lucide:check" width={14} height={14} className="context-menu-item-check" />
317
- )}
318
- {option.shortcut && !hasChildren && (
319
- <span className="context-menu-item-shortcut">
320
- {option.shortcut}
321
- </span>
322
- )}
323
- {hasChildren && (
324
- <Icon icon="lucide:chevron-right" width={14} height={14} className="context-menu-item-arrow" />
325
- )}
326
- </div>
327
- );
328
- })}
329
- </div>
330
-
331
- {/* 二级菜单(独立渲染,避免定位问题) */}
332
- {hoveredItem && submenuPosition && (
333
- <div
334
- className="context-menu context-menu-submenu"
335
- style={{
336
- left: `${submenuPosition.x}px`,
337
- top: `${submenuPosition.y}px`,
338
- }}
339
- onMouseEnter={() => setHoveredItemId(hoveredItem.id)}
340
- onMouseLeave={() => setHoveredItemId(null)}
341
- >
342
- {hoveredItem.children!.map((child, childIndex) => {
343
- if (child.separator) {
344
- return <div key={`child-sep-${childIndex}`} className="context-menu-separator" />;
345
- }
346
-
347
- return (
348
- <div
349
- key={child.id || childIndex}
350
- className={`context-menu-item ${
351
- child.disabled ? 'context-menu-item--disabled' : ''
352
- } ${
353
- child.danger ? 'context-menu-item--danger' : ''
354
- }`}
355
- onClick={() => handleOptionClick(child)}
356
- >
357
- {child.icon && renderIcon(child.icon)}
358
- <span className="context-menu-item-label">{child.label}</span>
359
- {child.checked && (
360
- <Icon icon="lucide:check" width={14} height={14} className="context-menu-item-check" />
361
- )}
362
- {child.shortcut && (
363
- <span className="context-menu-item-shortcut">
364
- {child.shortcut}
365
- </span>
366
- )}
367
- </div>
368
- );
369
- })}
370
- </div>
371
- )}
372
- </div>,
373
- document.body
374
- );
375
- }
@@ -1,239 +0,0 @@
1
- /**
2
- * FileGrid - 网格视图样式
3
- * 参考 macOS Finder 和 Windows Explorer 的设计
4
- */
5
-
6
- .file-grid {
7
- display: grid;
8
- /* macOS Finder 默认约 90-100px */
9
- grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
10
- gap: 6px;
11
- padding: 12px;
12
- align-content: start;
13
- grid-auto-rows: min-content;
14
- overflow-y: auto;
15
- overflow-x: hidden;
16
- }
17
-
18
- /* 网格项 - 更紧凑的布局 */
19
- .file-grid-item {
20
- display: flex;
21
- flex-direction: column;
22
- align-items: center;
23
- padding: 4px;
24
- border-radius: 6px;
25
- cursor: pointer;
26
- border: 1px solid transparent;
27
- transition: background-color 120ms ease, border-color 120ms ease;
28
- position: relative;
29
- overflow: hidden;
30
- min-width: 0;
31
- width: 100%;
32
- }
33
-
34
- /* 拖拽时的鼠标样式 */
35
- .file-grid-item[draggable="true"] {
36
- cursor: grab;
37
- }
38
-
39
- .file-grid-item:active {
40
- cursor: grabbing;
41
- }
42
-
43
- /* 悬停效果 - 类似 macOS */
44
- .file-grid-item--normal:hover {
45
- background: var(--huyooo-muted-hover);
46
- }
47
-
48
- /* 选中状态 - macOS 风格的蓝色高亮 */
49
- .file-grid-item--selected {
50
- background: var(--huyooo-focus-ring);
51
- border-color: var(--huyooo-primary);
52
- }
53
-
54
- .file-grid-item--selected:hover {
55
- background: var(--huyooo-focus-ring);
56
- }
57
-
58
- /* 拖拽悬停 */
59
- .file-grid-item--drag-over {
60
- background: var(--huyooo-focus-ring);
61
- border-color: var(--huyooo-primary);
62
- }
63
-
64
- /* 图标/缩略图容器 - 统一尺寸 */
65
- .file-grid-item-icon {
66
- position: relative;
67
- pointer-events: none;
68
- display: flex;
69
- align-items: center;
70
- justify-content: center;
71
- /* 统一 48x48 尺寸,更紧凑 */
72
- width: 48px;
73
- height: 48px;
74
- flex-shrink: 0;
75
- }
76
-
77
- /* 缩略图样式 - 统一尺寸和圆角 */
78
- /* 缩略图保持原图比例,使用 object-fit: cover 填充固定尺寸容器 */
79
- .file-grid-item-thumbnail {
80
- width: 48px;
81
- height: 48px;
82
- object-fit: cover;
83
- object-position: center;
84
- border-radius: 4px;
85
- /* 轻微阴影增加层次感 */
86
- box-shadow: var(--huyooo-shadow-sm);
87
- background: var(--huyooo-muted);
88
- /* 确保图片能作为加载占位符,平滑过渡到原图 */
89
- display: block;
90
- }
91
-
92
- /* 应用程序图标 - 无背景 */
93
- .file-grid-item-thumbnail--application {
94
- object-fit: contain;
95
- background: transparent;
96
- box-shadow: none;
97
- border-radius: 10px;
98
- }
99
-
100
- .file-grid-item-thumbnail--selected {
101
- box-shadow: 0 0 0 2px var(--huyooo-primary), 0 0 0 6px var(--huyooo-focus-ring);
102
- }
103
-
104
- /* 视频缩略图容器 */
105
- .file-grid-item-thumbnail--video {
106
- position: relative;
107
- border-radius: 4px;
108
- overflow: hidden;
109
- background: var(--huyooo-overlay-strong);
110
- width: 48px;
111
- height: 48px;
112
- box-shadow: var(--huyooo-shadow-sm);
113
- display: flex;
114
- align-items: center;
115
- justify-content: center;
116
- }
117
-
118
- /* 视频缩略图内的图片 - 保持比例,填充容器 */
119
- .file-grid-item-thumbnail--video > img {
120
- width: 100%;
121
- height: 100%;
122
- object-fit: cover;
123
- object-position: center;
124
- display: block;
125
- }
126
-
127
- /* 视频播放图标 */
128
- .file-grid-item-video-play {
129
- position: absolute;
130
- inset: 0;
131
- display: flex;
132
- align-items: center;
133
- justify-content: center;
134
- pointer-events: none;
135
- opacity: 0.9;
136
- transition: opacity 150ms;
137
- }
138
-
139
- .file-grid-item-thumbnail--video:hover .file-grid-item-video-play {
140
- opacity: 0;
141
- }
142
-
143
- .file-grid-item-video-play-icon {
144
- width: 20px;
145
- height: 20px;
146
- border-radius: 50%;
147
- background: var(--huyooo-overlay);
148
- backdrop-filter: blur(4px);
149
- display: flex;
150
- align-items: center;
151
- justify-content: center;
152
- }
153
-
154
- .file-grid-item-video-play-icon::before {
155
- content: "";
156
- width: 0;
157
- height: 0;
158
- border-top: 4px solid transparent;
159
- border-left: 7px solid white;
160
- border-bottom: 4px solid transparent;
161
- margin-left: 2px;
162
- }
163
-
164
- /* 文件名容器 */
165
- .file-grid-item-name-wrapper {
166
- display: flex;
167
- align-items: flex-start;
168
- justify-content: center;
169
- width: 100%;
170
- min-height: 0;
171
- }
172
-
173
- /* 文件名 - macOS 风格,最多2行,保留扩展名 */
174
- .file-grid-item-name {
175
- font-size: 12px;
176
- line-height: 1.3;
177
- text-align: center;
178
- padding: 2px;
179
- border-radius: 3px;
180
- user-select: none;
181
- cursor: default;
182
- transition: color 100ms;
183
- color: var(--huyooo-text);
184
- width: 100%;
185
- /* 使用 flexbox 布局,让扩展名始终显示 */
186
- display: flex;
187
- flex-direction: column;
188
- align-items: center;
189
- justify-content: flex-start;
190
- min-height: 0;
191
- }
192
-
193
- /* 文件名主体部分 - 可换行,最多1行 */
194
- .file-grid-item-name-base {
195
- display: -webkit-box;
196
- -webkit-line-clamp: 1;
197
- -webkit-box-orient: vertical;
198
- overflow: hidden;
199
- word-break: break-word;
200
- text-overflow: ellipsis;
201
- width: 100%;
202
- text-align: center;
203
- }
204
-
205
- /* 无扩展名:文件名允许两行(总高度仍控制在两行内) */
206
- .file-grid-item-name-base--two-lines {
207
- -webkit-line-clamp: 2;
208
- }
209
-
210
- /* 扩展名部分 - 始终显示,不换行 */
211
- .file-grid-item-name-ext {
212
- display: block;
213
- white-space: nowrap;
214
- flex-shrink: 0;
215
- text-align: center;
216
- }
217
-
218
- /* 选中后点击文件名可编辑 */
219
- .file-grid-item-name--selected {
220
- cursor: text;
221
- }
222
-
223
- /* 重命名输入框 */
224
- .file-grid-item-rename-input {
225
- width: 100%;
226
- font-size: 11px;
227
- line-height: 1.3;
228
- text-align: center;
229
- padding: 2px 4px;
230
- border: 1px solid var(--huyooo-primary);
231
- border-radius: 3px;
232
- outline: none;
233
- box-shadow: 0 0 0 3px var(--huyooo-focus-ring);
234
- background: var(--huyooo-surface-2);
235
- color: var(--huyooo-text);
236
- cursor: text;
237
- }
238
-
239
- /* 深色模式由 token 负责 */