@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,278 +0,0 @@
1
- import { useRef, useMemo } from 'react';
2
- import type { FileItem } from '../types';
3
- import { FileType } from '../types';
4
- import { FileIcon } from './FileIcon';
5
- import './FileGrid.css';
6
-
7
- /**
8
- * 分离文件名和扩展名
9
- * 文件夹或无扩展名文件返回 { baseName: name, ext: '' }
10
- */
11
- function splitFileName(name: string, isFolder: boolean): { baseName: string; ext: string } {
12
- if (isFolder) {
13
- return { baseName: name, ext: '' };
14
- }
15
- const lastDot = name.lastIndexOf('.');
16
- // 没有扩展名,或者是隐藏文件(如 .gitignore)
17
- if (lastDot <= 0) {
18
- return { baseName: name, ext: '' };
19
- }
20
- return {
21
- baseName: name.substring(0, lastDot),
22
- ext: name.substring(lastDot), // 包含点号
23
- };
24
- }
25
-
26
- /**
27
- * 文件名组件 - 保留扩展名显示,只截断文件名部分
28
- */
29
- function FileName({
30
- name,
31
- isFolder,
32
- isSelected,
33
- onClick
34
- }: {
35
- name: string;
36
- isFolder: boolean;
37
- isSelected: boolean;
38
- onClick: (e: React.MouseEvent) => void;
39
- }) {
40
- const { baseName, ext } = useMemo(() => splitFileName(name, isFolder), [name, isFolder]);
41
-
42
- return (
43
- <span
44
- onClick={onClick}
45
- className={`file-grid-item-name ${isSelected ? 'file-grid-item-name--selected' : ''}`}
46
- title={name}
47
- >
48
- {ext ? (
49
- <>
50
- <span className="file-grid-item-name-base">{baseName}</span>
51
- <span className="file-grid-item-name-ext">{ext}</span>
52
- </>
53
- ) : (
54
- // 无扩展名(包含文件夹/无扩展名文件/隐藏文件等):允许两行并省略
55
- <span className="file-grid-item-name-base file-grid-item-name-base--two-lines">{baseName}</span>
56
- )}
57
- </span>
58
- );
59
- }
60
-
61
- interface FileGridProps {
62
- items: FileItem[];
63
- selectedIds: Set<string>;
64
- editingId?: string | null;
65
- dragOverId?: string | null;
66
- getAppIconUrl?: (item: FileItem) => string | undefined;
67
- onSelect?: (item: FileItem, e: React.MouseEvent) => void;
68
- onOpen?: (item: FileItem) => void;
69
- onContextMenu?: (item: FileItem, e: React.MouseEvent) => void;
70
- onContextMenuEmpty?: (e: React.MouseEvent) => void;
71
- onNameClick?: (item: FileItem, e: React.MouseEvent) => void;
72
- onRename?: (item: FileItem, newName: string) => void;
73
- onRenameCancel?: (item: FileItem) => void;
74
- onDragStart?: (e: React.DragEvent, item: FileItem) => void;
75
- onDragOver?: (e: React.DragEvent, item: FileItem) => void;
76
- onDragLeave?: (e: React.DragEvent) => void;
77
- onDrop?: (e: React.DragEvent, item: FileItem) => void;
78
- onThumbnailError?: (item: FileItem, e: React.SyntheticEvent<HTMLImageElement, Event>) => void;
79
- }
80
-
81
- export function FileGrid({
82
- items,
83
- selectedIds,
84
- editingId,
85
- dragOverId,
86
- getAppIconUrl,
87
- onSelect,
88
- onOpen,
89
- onContextMenu,
90
- onContextMenuEmpty,
91
- onNameClick,
92
- onRename,
93
- onRenameCancel,
94
- onDragStart,
95
- onDragOver,
96
- onDragLeave,
97
- onDrop,
98
- onThumbnailError,
99
- }: FileGridProps) {
100
- const renameInputRef = useRef<HTMLInputElement>(null);
101
-
102
- /**
103
- * 空白处右键菜单
104
- */
105
- const handleEmptyContextMenu = (e: React.MouseEvent) => {
106
- const target = e.target as HTMLElement;
107
- if (!target.closest('.file-grid-item')) {
108
- onContextMenuEmpty?.(e);
109
- }
110
- };
111
-
112
- /**
113
- * 判断是否有缩略图或应用程序图标
114
- */
115
- const hasThumbnail = (item: FileItem): boolean => {
116
- // 应用程序图标
117
- if (item.type === FileType.APPLICATION && getAppIconUrl?.(item)) {
118
- return true;
119
- }
120
- // 图片:必须有缩略图才显示,永远不显示原始 URL
121
- if (item.type === FileType.IMAGE) {
122
- return !!item.thumbnailUrl;
123
- }
124
- // 视频:必须有缩略图才显示,如果没有缩略图说明不应该被识别为视频
125
- if (item.type === FileType.VIDEO) {
126
- return !!item.thumbnailUrl;
127
- }
128
- return false;
129
- };
130
-
131
- /**
132
- * 视频悬停播放
133
- */
134
- const handleVideoHover = (e: React.SyntheticEvent<HTMLVideoElement>, isHover: boolean) => {
135
- const video = e.currentTarget;
136
- if (isHover) {
137
- video.play().catch(() => {});
138
- } else {
139
- video.pause();
140
- video.currentTime = 0;
141
- }
142
- };
143
-
144
- /**
145
- * 处理重命名(blur 事件)
146
- */
147
- const handleRename = (item: FileItem, e: React.FocusEvent<HTMLInputElement>) => {
148
- const input = e.target;
149
- const newName = input.value.trim();
150
- if (newName && newName !== item.name) {
151
- onRename?.(item, newName);
152
- } else {
153
- onRenameCancel?.(item);
154
- }
155
- };
156
-
157
- /**
158
- * Enter 键保存
159
- */
160
- const handleEnterKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
161
- e.currentTarget.blur();
162
- };
163
-
164
- /**
165
- * Escape 键取消
166
- */
167
- const handleEscapeKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
168
- const input = e.currentTarget;
169
- const item = items.find((i) => i.id === editingId);
170
- if (item) {
171
- input.value = item.name;
172
- }
173
- input.blur();
174
- };
175
-
176
- return (
177
- <div className="file-grid" onContextMenu={(e) => { e.preventDefault(); handleEmptyContextMenu(e); }}>
178
- {items.map((item) => {
179
- const isSelected = selectedIds.has(item.id);
180
- const isEditing = editingId === item.id;
181
- const isDragOver = dragOverId === item.id;
182
- const hasThumb = hasThumbnail(item);
183
- const appIconUrl = item.type === FileType.APPLICATION ? getAppIconUrl?.(item) : undefined;
184
-
185
- let itemClassName = 'file-grid-item';
186
- if (isSelected && !isEditing) {
187
- itemClassName += ' file-grid-item--selected';
188
- } else if (isDragOver) {
189
- itemClassName += ' file-grid-item--drag-over';
190
- } else {
191
- itemClassName += ' file-grid-item--normal';
192
- }
193
-
194
- return (
195
- <div
196
- key={item.id}
197
- draggable={!isEditing}
198
- onDragStart={(e) => onDragStart?.(e, item)}
199
- onDragOver={(e) => { e.preventDefault(); onDragOver?.(e, item); }}
200
- onDragLeave={(e) => onDragLeave?.(e)}
201
- onDrop={(e) => { e.preventDefault(); onDrop?.(e, item); }}
202
- onClick={(e) => { e.stopPropagation(); onSelect?.(item, e); }}
203
- onDoubleClick={(e) => { e.stopPropagation(); onOpen?.(item); }}
204
- onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu?.(item, e); }}
205
- className={itemClassName}
206
- >
207
- <div className="file-grid-item-icon">
208
- {/* 应用程序图标 */}
209
- {appIconUrl && (
210
- <img
211
- src={appIconUrl}
212
- alt={item.name}
213
- className={`file-grid-item-thumbnail file-grid-item-thumbnail--application ${
214
- isSelected && !isEditing ? 'file-grid-item-thumbnail--selected' : ''
215
- }`}
216
- />
217
- )}
218
- {/* 视频缩略图 - 视频类型必须有缩略图,如果没有缩略图说明不应该被识别为视频,按普通文件处理 */}
219
- {!appIconUrl && item.type === FileType.VIDEO && item.thumbnailUrl && hasThumb && (
220
- <div className="file-grid-item-thumbnail file-grid-item-thumbnail--video">
221
- <img
222
- src={item.thumbnailUrl}
223
- alt={item.name}
224
- className="file-grid-item-thumbnail"
225
- onError={(e) => onThumbnailError?.(item, e)}
226
- />
227
- <div className="file-grid-item-video-play">
228
- <div className="file-grid-item-video-play-icon" />
229
- </div>
230
- </div>
231
- )}
232
- {/* 图片缩略图 - 永远只显示缩略图,不显示原始 URL */}
233
- {!appIconUrl && item.type === FileType.IMAGE && item.thumbnailUrl && hasThumb && (
234
- <img
235
- src={item.thumbnailUrl}
236
- alt={item.name}
237
- className="file-grid-item-thumbnail"
238
- onError={(e) => onThumbnailError?.(item, e)}
239
- />
240
- )}
241
- {/* 默认图标 */}
242
- {!appIconUrl && !hasThumb && (
243
- <FileIcon type={item.type} name={item.name} size={48} />
244
- )}
245
- </div>
246
-
247
- <div className="file-grid-item-name-wrapper">
248
- {isEditing ? (
249
- <input
250
- ref={renameInputRef}
251
- type="text"
252
- className="file-grid-item-rename-input"
253
- defaultValue={item.name}
254
- onBlur={(e) => handleRename(item, e)}
255
- onKeyDown={(e) => {
256
- if (e.key === 'Enter') {
257
- handleEnterKey(e);
258
- } else if (e.key === 'Escape') {
259
- handleEscapeKey(e);
260
- }
261
- }}
262
- autoFocus
263
- />
264
- ) : (
265
- <FileName
266
- name={item.name}
267
- isFolder={item.type === FileType.FOLDER}
268
- isSelected={isSelected}
269
- onClick={(e) => { e.stopPropagation(); onNameClick?.(item, e); }}
270
- />
271
- )}
272
- </div>
273
- </div>
274
- );
275
- })}
276
- </div>
277
- );
278
- }
@@ -1,41 +0,0 @@
1
- .file-icon--folder {
2
- color: var(--huyooo-primary);
3
- fill: var(--huyooo-primary);
4
- }
5
-
6
- .file-icon--image {
7
- color: var(--huyooo-primary);
8
- }
9
-
10
- .file-icon--text {
11
- color: var(--huyooo-text-muted);
12
- }
13
-
14
- .file-icon--code {
15
- color: var(--huyooo-success);
16
- }
17
-
18
- .file-icon--music {
19
- color: var(--huyooo-danger);
20
- }
21
-
22
- .file-icon--video {
23
- color: var(--huyooo-primary);
24
- }
25
-
26
- .file-icon--pdf {
27
- color: var(--huyooo-danger);
28
- }
29
-
30
- .file-icon--application {
31
- color: var(--huyooo-primary);
32
- }
33
-
34
- .file-icon--archive {
35
- color: var(--huyooo-warning);
36
- }
37
-
38
- .file-icon--default {
39
- color: var(--huyooo-text-disabled);
40
- }
41
-
@@ -1,86 +0,0 @@
1
- import { useMemo } from 'react';
2
- import { Icon } from '@iconify/react';
3
- import { FileType, type FileType as FileTypeType } from '../types';
4
- import { getFileTypeIcon } from '../utils/fileTypeIcon';
5
- import { getFolderTypeIcon } from '../utils/folderTypeIcon';
6
- import './FileIcon.css';
7
-
8
- interface FileIconProps {
9
- type: FileTypeType;
10
- name?: string;
11
- className?: string;
12
- size?: number;
13
- }
14
-
15
- export function FileIcon({ type, name, className = '', size = 24 }: FileIconProps) {
16
- const iconName = useMemo(() => {
17
- if (type === FileType.FOLDER) {
18
- // 文件夹:先查特定类型图标,否则用默认 folder
19
- const folderIcon = name ? getFolderTypeIcon(name) : undefined;
20
- return folderIcon ?? 'ic:round-folder';
21
- }
22
-
23
- // 如果有文件名,使用 material-icon-theme 图标映射(传递 type 作为兜底)
24
- if (name) {
25
- return getFileTypeIcon(name, type);
26
- }
27
-
28
- // 回退逻辑(没有文件名时)
29
- switch (type) {
30
- case FileType.IMAGE:
31
- return 'material-icon-theme:image';
32
- case FileType.TEXT:
33
- return 'material-icon-theme:document';
34
- case FileType.CODE:
35
- return 'material-icon-theme:javascript';
36
- case FileType.MUSIC:
37
- return 'material-icon-theme:audio';
38
- case FileType.VIDEO:
39
- return 'material-icon-theme:video';
40
- case FileType.PDF:
41
- return 'material-icon-theme:pdf';
42
- case FileType.DOCUMENT:
43
- return 'material-icon-theme:word';
44
- case FileType.APPLICATION:
45
- return 'material-icon-theme:exe';
46
- case FileType.ARCHIVE:
47
- return 'material-icon-theme:zip';
48
- default:
49
- return 'material-icon-theme:document';
50
- }
51
- }, [type, name]);
52
-
53
- const iconClass = useMemo(() => {
54
- const base = 'file-icon';
55
- switch (type) {
56
- case FileType.FOLDER:
57
- return `${base} file-icon--folder`;
58
- case FileType.IMAGE:
59
- return `${base} file-icon--image`;
60
- case FileType.TEXT:
61
- return `${base} file-icon--text`;
62
- case FileType.CODE:
63
- return `${base} file-icon--code`;
64
- case FileType.MUSIC:
65
- return `${base} file-icon--music`;
66
- case FileType.VIDEO:
67
- return `${base} file-icon--video`;
68
- case FileType.PDF:
69
- case FileType.DOCUMENT:
70
- return `${base} file-icon--pdf`;
71
- case FileType.APPLICATION:
72
- return `${base} file-icon--application`;
73
- case FileType.ARCHIVE:
74
- return `${base} file-icon--archive`;
75
- default:
76
- return `${base} file-icon--default`;
77
- }
78
- }, [type]);
79
-
80
- return (
81
- <div className={className}>
82
- <Icon icon={iconName} width={size} height={size} className={iconClass} />
83
- </div>
84
- );
85
- }
86
-
@@ -1,214 +0,0 @@
1
- /* FileInfoDialog 样式 */
2
-
3
- .file-info-dialog-overlay {
4
- position: fixed;
5
- inset: 0;
6
- background: var(--huyooo-overlay);
7
- display: flex;
8
- align-items: center;
9
- justify-content: center;
10
- z-index: 10000;
11
- animation: file-info-fadeIn 0.15s ease-out;
12
- }
13
-
14
- @keyframes file-info-fadeIn {
15
- from { opacity: 0; }
16
- to { opacity: 1; }
17
- }
18
-
19
- .file-info-dialog {
20
- background: var(--huyooo-panel-bg);
21
- border: 1px solid var(--huyooo-border);
22
- border-radius: 12px;
23
- width: 420px;
24
- max-width: 90vw;
25
- max-height: 80vh;
26
- display: flex;
27
- flex-direction: column;
28
- box-shadow: var(--huyooo-menu-shadow);
29
- animation: file-info-slideIn 0.2s ease-out;
30
- }
31
-
32
- @keyframes file-info-slideIn {
33
- from {
34
- opacity: 0;
35
- transform: scale(0.95) translateY(-10px);
36
- }
37
- to {
38
- opacity: 1;
39
- transform: scale(1) translateY(0);
40
- }
41
- }
42
-
43
- /* 深色模式由 token 负责 */
44
-
45
- /* 头部 */
46
- .file-info-dialog-header {
47
- display: flex;
48
- align-items: center;
49
- justify-content: space-between;
50
- padding: 16px 20px;
51
- border-bottom: 1px solid var(--huyooo-border);
52
- gap: 12px;
53
- }
54
-
55
- /* 深色模式由 token 负责 */
56
-
57
- .file-info-dialog-title {
58
- display: flex;
59
- align-items: center;
60
- gap: 10px;
61
- min-width: 0;
62
- flex: 1;
63
- }
64
-
65
- .file-info-dialog-name {
66
- font-size: 16px;
67
- font-weight: 600;
68
- color: var(--huyooo-text);
69
- white-space: nowrap;
70
- overflow: hidden;
71
- text-overflow: ellipsis;
72
- }
73
-
74
- /* 深色模式由 token 负责 */
75
-
76
- .file-info-dialog-close {
77
- display: flex;
78
- align-items: center;
79
- justify-content: center;
80
- width: 28px;
81
- height: 28px;
82
- border: none;
83
- background: transparent;
84
- color: var(--huyooo-text-muted);
85
- border-radius: 6px;
86
- cursor: pointer;
87
- flex-shrink: 0;
88
- transition: all 0.15s ease;
89
- }
90
-
91
- .file-info-dialog-close:hover {
92
- background: var(--huyooo-muted);
93
- color: var(--huyooo-text);
94
- }
95
-
96
- /* 深色模式由 token 负责 */
97
-
98
- /* 内容 */
99
- .file-info-dialog-content {
100
- padding: 16px 20px;
101
- display: flex;
102
- flex-direction: column;
103
- gap: 12px;
104
- overflow-y: auto;
105
- }
106
-
107
- .file-info-row {
108
- display: flex;
109
- align-items: flex-start;
110
- gap: 16px;
111
- }
112
-
113
- .file-info-label {
114
- display: flex;
115
- align-items: center;
116
- gap: 6px;
117
- width: 90px;
118
- flex-shrink: 0;
119
- color: var(--huyooo-text-muted);
120
- font-size: 13px;
121
- }
122
-
123
- /* 深色模式由 token 负责 */
124
-
125
- .file-info-label svg {
126
- flex-shrink: 0;
127
- }
128
-
129
- .file-info-value {
130
- flex: 1;
131
- color: var(--huyooo-text);
132
- font-size: 13px;
133
- word-break: break-all;
134
- line-height: 1.4;
135
- }
136
-
137
- /* 深色模式由 token 负责 */
138
-
139
- .file-info-value--path {
140
- font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
141
- font-size: 12px;
142
- background: var(--huyooo-code-bg);
143
- color: var(--huyooo-code-text);
144
- padding: 4px 8px;
145
- border-radius: 4px;
146
- user-select: all;
147
- }
148
-
149
- /* 深色模式由 token 负责 */
150
-
151
- /* 图标样式 */
152
- .file-info-icon {
153
- width: 32px;
154
- height: 32px;
155
- flex-shrink: 0;
156
- }
157
-
158
- .file-info-icon--folder {
159
- color: var(--huyooo-filetype-folder);
160
- }
161
-
162
- .file-info-icon--image {
163
- color: var(--huyooo-filetype-image);
164
- }
165
-
166
- .file-info-icon--video {
167
- color: var(--huyooo-filetype-video);
168
- }
169
-
170
- .file-info-icon--music {
171
- color: var(--huyooo-filetype-music);
172
- }
173
-
174
- .file-info-icon--document {
175
- color: var(--huyooo-filetype-document);
176
- }
177
-
178
- .file-info-icon--code {
179
- color: var(--huyooo-filetype-code);
180
- }
181
-
182
- .file-info-icon--archive {
183
- color: var(--huyooo-filetype-archive);
184
- }
185
-
186
- .file-info-icon--file {
187
- color: var(--huyooo-filetype-file);
188
- }
189
-
190
- /* 底部 */
191
- .file-info-dialog-footer {
192
- display: flex;
193
- justify-content: flex-end;
194
- padding: 12px 20px;
195
- border-top: 1px solid var(--huyooo-border);
196
- }
197
-
198
- /* 深色模式由 token 负责 */
199
-
200
- .file-info-dialog-btn {
201
- padding: 6px 16px;
202
- border: none;
203
- border-radius: 6px;
204
- font-size: 13px;
205
- cursor: pointer;
206
- transition: all 0.15s ease;
207
- background: var(--huyooo-primary);
208
- color: white;
209
- }
210
-
211
- .file-info-dialog-btn:hover {
212
- background: var(--huyooo-primary-hover, var(--huyooo-primary));
213
- }
214
-