@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,66 @@
1
+ import { Icon } from '@iconify/react';
2
+ import type { SidebarItem } from '../types';
3
+ import './FileSidebar.css';
4
+
5
+ interface SidebarSection {
6
+ id: string;
7
+ title: string;
8
+ items: SidebarItem[];
9
+ }
10
+
11
+ interface FileSidebarProps {
12
+ sections: SidebarSection[];
13
+ activeId?: string;
14
+ onNavigate?: (item: SidebarItem) => void;
15
+ }
16
+
17
+ export function FileSidebar({ sections, activeId, onNavigate }: FileSidebarProps) {
18
+ const handleNavigate = (item: SidebarItem) => {
19
+ onNavigate?.(item);
20
+ };
21
+
22
+ // 将 Lucide 图标名称转换为 Iconify 格式
23
+ const getIconName = (iconName?: string): string => {
24
+ if (!iconName) return 'mdi:folder';
25
+ // 如果已经是 Iconify 格式,直接返回
26
+ if (iconName.includes(':')) return iconName;
27
+ // 否则转换为 lucide: 格式
28
+ return `lucide:${iconName.toLowerCase()}`;
29
+ };
30
+
31
+ return (
32
+ <div className="file-sidebar">
33
+ {sections.map((section) => (
34
+ <div key={section.id} className="file-sidebar-section">
35
+ <div className="file-sidebar-section-title">{section.title}</div>
36
+ <ul className="file-sidebar-list">
37
+ {section.items.map((item) => {
38
+ const isActive = activeId === item.id;
39
+ return (
40
+ <li
41
+ key={item.id}
42
+ onClick={() => handleNavigate(item)}
43
+ className={`file-sidebar-item ${
44
+ isActive ? 'file-sidebar-item--active' : ''
45
+ }`}
46
+ >
47
+ <Icon
48
+ icon={getIconName(item.icon)}
49
+ width={18}
50
+ height={18}
51
+ className={
52
+ isActive
53
+ ? 'file-sidebar-item-icon--active'
54
+ : 'file-sidebar-item-icon'
55
+ }
56
+ />
57
+ <span>{item.label}</span>
58
+ </li>
59
+ );
60
+ })}
61
+ </ul>
62
+ </div>
63
+ ))}
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,211 @@
1
+ .progress-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: 10001;
9
+ }
10
+
11
+ .progress-dialog {
12
+ background: white;
13
+ border-radius: 12px;
14
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
15
+ width: 380px;
16
+ max-width: 90vw;
17
+ overflow: hidden;
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+
22
+ .progress-dialog-header {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ padding: 16px 20px;
27
+ border-bottom: 1px solid rgb(229, 231, 233);
28
+ }
29
+
30
+ .progress-dialog-title {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: 10px;
34
+ font-weight: 600;
35
+ font-size: 16px;
36
+ color: rgb(17, 24, 39);
37
+ }
38
+
39
+ .progress-dialog-close {
40
+ background: none;
41
+ border: none;
42
+ padding: 4px;
43
+ cursor: pointer;
44
+ color: rgb(107, 114, 128);
45
+ border-radius: 4px;
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ }
50
+
51
+ .progress-dialog-close:hover {
52
+ background: rgb(243, 244, 246);
53
+ color: rgb(55, 65, 81);
54
+ }
55
+
56
+ .progress-dialog-content {
57
+ padding: 24px 20px;
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 16px;
61
+ }
62
+
63
+ .progress-dialog-status {
64
+ font-size: 14px;
65
+ color: rgb(55, 65, 81);
66
+ text-align: center;
67
+ }
68
+
69
+ .progress-dialog-bar-container {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 12px;
73
+ }
74
+
75
+ .progress-dialog-bar {
76
+ flex: 1;
77
+ height: 8px;
78
+ background: rgb(229, 231, 233);
79
+ border-radius: 4px;
80
+ overflow: hidden;
81
+ }
82
+
83
+ .progress-dialog-bar-fill {
84
+ height: 100%;
85
+ background: rgb(59, 130, 246);
86
+ border-radius: 4px;
87
+ transition: width 0.3s ease;
88
+ }
89
+
90
+ .progress-dialog-percent {
91
+ font-size: 13px;
92
+ font-weight: 500;
93
+ color: rgb(55, 65, 81);
94
+ min-width: 40px;
95
+ text-align: right;
96
+ }
97
+
98
+ .progress-dialog-current-file {
99
+ font-size: 12px;
100
+ color: rgb(107, 114, 128);
101
+ text-align: center;
102
+ white-space: nowrap;
103
+ overflow: hidden;
104
+ text-overflow: ellipsis;
105
+ }
106
+
107
+ .progress-dialog-count {
108
+ font-size: 12px;
109
+ color: rgb(107, 114, 128);
110
+ text-align: center;
111
+ }
112
+
113
+ .progress-dialog-error {
114
+ padding: 12px;
115
+ background: rgb(254, 242, 242);
116
+ border: 1px solid rgb(254, 202, 202);
117
+ border-radius: 6px;
118
+ color: rgb(185, 28, 28);
119
+ font-size: 13px;
120
+ }
121
+
122
+ .progress-dialog-output {
123
+ padding: 12px;
124
+ background: rgb(240, 253, 244);
125
+ border: 1px solid rgb(187, 247, 208);
126
+ border-radius: 6px;
127
+ font-size: 12px;
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 4px;
131
+ }
132
+
133
+ .progress-dialog-output-label {
134
+ color: rgb(22, 101, 52);
135
+ font-weight: 500;
136
+ }
137
+
138
+ .progress-dialog-output-path {
139
+ color: rgb(21, 128, 61);
140
+ word-break: break-all;
141
+ }
142
+
143
+ .progress-dialog-footer {
144
+ display: flex;
145
+ justify-content: flex-end;
146
+ gap: 12px;
147
+ padding: 16px 20px;
148
+ border-top: 1px solid rgb(229, 231, 233);
149
+ }
150
+
151
+ .progress-dialog-btn {
152
+ padding: 8px 16px;
153
+ border-radius: 6px;
154
+ font-size: 14px;
155
+ font-weight: 500;
156
+ cursor: pointer;
157
+ transition: all 0.2s;
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 6px;
161
+ }
162
+
163
+ .progress-dialog-btn-cancel {
164
+ background: white;
165
+ border: 1px solid rgb(209, 213, 219);
166
+ color: rgb(55, 65, 81);
167
+ }
168
+
169
+ .progress-dialog-btn-cancel:hover {
170
+ background: rgb(249, 250, 251);
171
+ }
172
+
173
+ .progress-dialog-btn-folder {
174
+ background: rgb(240, 253, 244);
175
+ border: 1px solid rgb(187, 247, 208);
176
+ color: rgb(22, 101, 52);
177
+ }
178
+
179
+ .progress-dialog-btn-folder:hover {
180
+ background: rgb(220, 252, 231);
181
+ }
182
+
183
+ .progress-dialog-btn-close {
184
+ background: rgb(59, 130, 246);
185
+ border: 1px solid rgb(59, 130, 246);
186
+ color: white;
187
+ }
188
+
189
+ .progress-dialog-btn-close:hover {
190
+ background: rgb(37, 99, 235);
191
+ }
192
+
193
+ /* 旋转动画 */
194
+ .progress-dialog-icon-spin {
195
+ animation: spin 1s linear infinite;
196
+ color: rgb(59, 130, 246);
197
+ }
198
+
199
+ @keyframes spin {
200
+ from { transform: rotate(0deg); }
201
+ to { transform: rotate(360deg); }
202
+ }
203
+
204
+ .progress-dialog-icon-success {
205
+ color: rgb(34, 197, 94);
206
+ }
207
+
208
+ .progress-dialog-icon-error {
209
+ color: rgb(239, 68, 68);
210
+ }
211
+
@@ -0,0 +1,183 @@
1
+ import { createPortal } from 'react-dom';
2
+ import { Icon } from '@iconify/react';
3
+ import './ProgressDialog.css';
4
+
5
+ /** 进度状态 */
6
+ export type ProgressStatus = 'pending' | 'processing' | 'success' | 'error';
7
+
8
+ /** 进度信息 */
9
+ export interface ProgressInfo {
10
+ /** 操作类型 */
11
+ type: 'compress' | 'extract';
12
+ /** 状态 */
13
+ status: ProgressStatus;
14
+ /** 进度百分比 0-100 */
15
+ percent: number;
16
+ /** 当前处理的文件 */
17
+ currentFile?: string;
18
+ /** 已处理文件数 */
19
+ processedCount?: number;
20
+ /** 总文件数 */
21
+ totalCount?: number;
22
+ /** 错误信息 */
23
+ error?: string;
24
+ /** 输出路径 */
25
+ outputPath?: string;
26
+ }
27
+
28
+ interface ProgressDialogProps {
29
+ visible: boolean;
30
+ progress: ProgressInfo;
31
+ onCancel?: () => void;
32
+ onClose?: () => void;
33
+ /** 打开输出文件夹 */
34
+ onOpenFolder?: (path: string) => void;
35
+ }
36
+
37
+ /**
38
+ * 进度对话框组件
39
+ */
40
+ export function ProgressDialog({
41
+ visible,
42
+ progress,
43
+ onCancel,
44
+ onClose,
45
+ onOpenFolder,
46
+ }: ProgressDialogProps) {
47
+ const { type, status, percent, currentFile, processedCount, totalCount, error, outputPath } = progress;
48
+
49
+ const title = type === 'compress' ? '压缩文件' : '解压文件';
50
+ const isCompleted = status === 'success' || status === 'error';
51
+
52
+ // 获取状态图标
53
+ const StatusIcon = () => {
54
+ switch (status) {
55
+ case 'processing':
56
+ return <Icon icon="lucide:loader-2" width={24} height={24} className="progress-dialog-icon-spin" />;
57
+ case 'success':
58
+ return <Icon icon="lucide:check-circle" width={24} height={24} className="progress-dialog-icon-success" />;
59
+ case 'error':
60
+ return <Icon icon="lucide:x-circle" width={24} height={24} className="progress-dialog-icon-error" />;
61
+ default:
62
+ return <Icon icon="lucide:archive" width={24} height={24} />;
63
+ }
64
+ };
65
+
66
+ // 获取状态文本
67
+ const getStatusText = () => {
68
+ switch (status) {
69
+ case 'pending':
70
+ return '准备中...';
71
+ case 'processing':
72
+ return type === 'compress' ? '正在压缩...' : '正在解压...';
73
+ case 'success':
74
+ return type === 'compress' ? '压缩完成' : '解压完成';
75
+ case 'error':
76
+ return '操作失败';
77
+ default:
78
+ return '';
79
+ }
80
+ };
81
+
82
+ const handleClose = () => {
83
+ if (isCompleted) {
84
+ onClose?.();
85
+ } else {
86
+ onCancel?.();
87
+ }
88
+ };
89
+
90
+ if (!visible) return null;
91
+
92
+ return createPortal(
93
+ <div className="progress-dialog-overlay">
94
+ <div className="progress-dialog">
95
+ {/* 头部 */}
96
+ <div className="progress-dialog-header">
97
+ <div className="progress-dialog-title">
98
+ <StatusIcon />
99
+ <span>{title}</span>
100
+ </div>
101
+ {isCompleted && (
102
+ <button className="progress-dialog-close" onClick={handleClose}>
103
+ <Icon icon="lucide:x" width={18} height={18} />
104
+ </button>
105
+ )}
106
+ </div>
107
+
108
+ {/* 内容 */}
109
+ <div className="progress-dialog-content">
110
+ {/* 状态文本 */}
111
+ <div className="progress-dialog-status">{getStatusText()}</div>
112
+
113
+ {/* 进度条 */}
114
+ {status === 'processing' && (
115
+ <div className="progress-dialog-bar-container">
116
+ <div className="progress-dialog-bar">
117
+ <div
118
+ className="progress-dialog-bar-fill"
119
+ style={{ width: `${percent}%` }}
120
+ />
121
+ </div>
122
+ <span className="progress-dialog-percent">{percent}%</span>
123
+ </div>
124
+ )}
125
+
126
+ {/* 当前文件 */}
127
+ {currentFile && status === 'processing' && (
128
+ <div className="progress-dialog-current-file">
129
+ {currentFile}
130
+ </div>
131
+ )}
132
+
133
+ {/* 文件计数 */}
134
+ {totalCount && totalCount > 0 && status === 'processing' && (
135
+ <div className="progress-dialog-count">
136
+ {processedCount || 0} / {totalCount} 个文件
137
+ </div>
138
+ )}
139
+
140
+ {/* 错误信息 */}
141
+ {error && (
142
+ <div className="progress-dialog-error">
143
+ {error}
144
+ </div>
145
+ )}
146
+
147
+ {/* 成功后显示输出路径 */}
148
+ {status === 'success' && outputPath && (
149
+ <div className="progress-dialog-output">
150
+ <span className="progress-dialog-output-label">输出位置:</span>
151
+ <span className="progress-dialog-output-path">{outputPath}</span>
152
+ </div>
153
+ )}
154
+ </div>
155
+
156
+ {/* 底部按钮 */}
157
+ <div className="progress-dialog-footer">
158
+ {status === 'processing' && onCancel && (
159
+ <button className="progress-dialog-btn progress-dialog-btn-cancel" onClick={onCancel}>
160
+ 取消
161
+ </button>
162
+ )}
163
+ {status === 'success' && outputPath && onOpenFolder && (
164
+ <button
165
+ className="progress-dialog-btn progress-dialog-btn-folder"
166
+ onClick={() => onOpenFolder(outputPath)}
167
+ >
168
+ <Icon icon="lucide:folder-open" width={16} height={16} />
169
+ 打开文件夹
170
+ </button>
171
+ )}
172
+ {isCompleted && (
173
+ <button className="progress-dialog-btn progress-dialog-btn-close" onClick={handleClose}>
174
+ 关闭
175
+ </button>
176
+ )}
177
+ </div>
178
+ </div>
179
+ </div>,
180
+ document.body
181
+ );
182
+ }
183
+
@@ -0,0 +1,7 @@
1
+ .sort-indicator {
2
+ margin-left: 4px;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ color: rgb(59, 130, 246);
6
+ }
7
+
@@ -0,0 +1,19 @@
1
+ import { Icon } from '@iconify/react';
2
+ import './SortIndicator.css';
3
+
4
+ interface SortIndicatorProps {
5
+ direction: 'asc' | 'desc';
6
+ }
7
+
8
+ export function SortIndicator({ direction }: SortIndicatorProps) {
9
+ return (
10
+ <span className="sort-indicator">
11
+ <Icon
12
+ icon={direction === 'asc' ? 'lucide:chevron-up' : 'lucide:chevron-down'}
13
+ width={12}
14
+ height={12}
15
+ />
16
+ </span>
17
+ );
18
+ }
19
+
@@ -0,0 +1,20 @@
1
+ .file-status-bar {
2
+ height: 1.5rem;
3
+ background-color: rgb(249, 250, 251);
4
+ border-top: 1px solid rgb(229, 231, 233);
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ padding: 0 8px;
9
+ font-size: 10px;
10
+ color: rgb(107, 114, 128);
11
+ user-select: none;
12
+ flex-shrink: 0;
13
+ z-index: 20;
14
+ gap: 4px;
15
+ }
16
+
17
+ .file-status-bar > * {
18
+ flex-shrink: 0;
19
+ }
20
+
@@ -0,0 +1,21 @@
1
+ import './StatusBar.css';
2
+
3
+ interface StatusBarProps {
4
+ itemCount?: number;
5
+ selectedCount?: number;
6
+ children?: React.ReactNode;
7
+ }
8
+
9
+ export function StatusBar({ itemCount = 0, selectedCount = 0, children }: StatusBarProps) {
10
+ return (
11
+ <div className="file-status-bar">
12
+ {children || (
13
+ <>
14
+ <span>{itemCount} 个项目</span>
15
+ {selectedCount > 0 && <span> • 已选择 {selectedCount} 个</span>}
16
+ </>
17
+ )}
18
+ </div>
19
+ );
20
+ }
21
+
@@ -0,0 +1,150 @@
1
+ .file-toolbar {
2
+ height: 3rem;
3
+ background: rgba(249, 250, 251, 0.9);
4
+ backdrop-filter: blur(12px);
5
+ border-bottom: 1px solid rgb(229, 231, 233);
6
+ display: flex;
7
+ align-items: center;
8
+ padding: 0 1rem;
9
+ user-select: none;
10
+ flex-shrink: 0;
11
+ z-index: 30;
12
+ position: relative;
13
+ }
14
+
15
+ .file-toolbar--draggable {
16
+ -webkit-app-region: drag;
17
+ }
18
+
19
+ .file-toolbar-nav {
20
+ display: flex;
21
+ align-items: center;
22
+ color: rgb(75, 85, 99);
23
+ background: rgba(229, 231, 233, 0.5);
24
+ border-radius: 0.5rem;
25
+ padding: 0.125rem;
26
+ flex-shrink: 0;
27
+ -webkit-app-region: no-drag;
28
+ }
29
+
30
+ .file-toolbar-button {
31
+ padding: 0.25rem;
32
+ border-radius: 0.375rem;
33
+ transition: all 200ms;
34
+ border: none;
35
+ background: transparent;
36
+ cursor: pointer;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ color: rgb(31, 41, 55);
41
+ }
42
+
43
+ .file-toolbar-button:hover:not(:disabled) {
44
+ background: rgb(209, 213, 219);
45
+ }
46
+
47
+ .file-toolbar-button:disabled {
48
+ color: rgb(156, 163, 175);
49
+ cursor: default;
50
+ }
51
+
52
+ .file-toolbar-button--active {
53
+ background: white;
54
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
55
+ color: rgb(31, 41, 55);
56
+ }
57
+
58
+ .file-toolbar-breadcrumb {
59
+ flex: 1;
60
+ min-width: 0;
61
+ overflow: hidden;
62
+ margin-left: 1rem;
63
+ /* 不设置 no-drag,让空白区域可拖拽 */
64
+ }
65
+
66
+ .file-toolbar-custom {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 0.5rem;
70
+ -webkit-app-region: no-drag;
71
+ }
72
+
73
+ .file-toolbar-actions {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 0.75rem;
77
+ flex-shrink: 0;
78
+ margin-left: auto;
79
+ -webkit-app-region: no-drag;
80
+ }
81
+
82
+ .file-toolbar-search {
83
+ position: relative;
84
+ display: none;
85
+ }
86
+
87
+ @media (min-width: 768px) {
88
+ .file-toolbar-search {
89
+ display: block;
90
+ }
91
+ }
92
+
93
+ .file-toolbar-search-icon {
94
+ position: absolute;
95
+ left: 0.625rem;
96
+ top: 50%;
97
+ transform: translateY(-50%);
98
+ color: rgb(156, 163, 175);
99
+ transition: color 200ms;
100
+ }
101
+
102
+ .file-toolbar-search:focus-within .file-toolbar-search-icon {
103
+ color: rgb(59, 130, 246);
104
+ }
105
+
106
+ .file-toolbar-search-input {
107
+ background: rgb(243, 244, 246);
108
+ border: 1px solid transparent;
109
+ border-radius: 0.375rem;
110
+ padding-left: 2rem;
111
+ padding-right: 0.75rem;
112
+ padding-top: 0.25rem;
113
+ padding-bottom: 0.25rem;
114
+ font-size: 0.875rem;
115
+ outline: none;
116
+ transition: all 200ms;
117
+ width: 8rem;
118
+ color: rgb(55, 65, 81);
119
+ }
120
+
121
+ .file-toolbar-search-input::placeholder {
122
+ color: rgb(156, 163, 175);
123
+ }
124
+
125
+ .file-toolbar-search-input:focus {
126
+ background: white;
127
+ border-color: rgb(96, 165, 250);
128
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
129
+ width: 12rem;
130
+ }
131
+
132
+ .file-toolbar-view-toggle {
133
+ display: flex;
134
+ align-items: center;
135
+ background: rgba(229, 231, 233, 0.5);
136
+ border-radius: 0.5rem;
137
+ padding: 0.125rem;
138
+ color: rgb(75, 85, 99);
139
+ }
140
+
141
+ .file-toolbar-view-toggle .file-toolbar-button--active {
142
+ background: white;
143
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
144
+ color: rgb(31, 41, 55);
145
+ }
146
+
147
+ .file-toolbar-view-toggle .file-toolbar-button:not(.file-toolbar-button--active):hover {
148
+ background: rgba(209, 213, 219, 0.5);
149
+ }
150
+