@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,274 @@
1
+ /**
2
+ * 文件类型(使用 const object 而非 enum,更好的 tree-shaking)
3
+ */
4
+ export const FileType = {
5
+ FOLDER: 'folder',
6
+ FILE: 'file',
7
+ IMAGE: 'image',
8
+ VIDEO: 'video',
9
+ MUSIC: 'music',
10
+ DOCUMENT: 'document',
11
+ CODE: 'code',
12
+ TEXT: 'text',
13
+ PDF: 'pdf',
14
+ ARCHIVE: 'archive',
15
+ APPLICATION: 'application',
16
+ UNKNOWN: 'unknown'
17
+ } as const;
18
+
19
+ export type FileType = typeof FileType[keyof typeof FileType];
20
+
21
+ /**
22
+ * 文件系统项
23
+ */
24
+ export interface FileItem {
25
+ /** 唯一标识(通常是完整路径) */
26
+ id: string;
27
+ name: string;
28
+ type: FileType;
29
+ size?: string;
30
+ dateModified?: string;
31
+ url?: string;
32
+ thumbnailUrl?: string;
33
+ children?: FileItem[];
34
+ }
35
+
36
+ /**
37
+ * 视图模式
38
+ */
39
+ export type ViewMode = 'grid' | 'list' | 'columns';
40
+
41
+ /**
42
+ * 排序配置
43
+ */
44
+ export interface SortConfig {
45
+ field: 'name' | 'dateModified' | 'size' | 'type';
46
+ direction: 'asc' | 'desc';
47
+ }
48
+
49
+ /**
50
+ * 侧边栏项目
51
+ */
52
+ export interface SidebarItem {
53
+ id: string;
54
+ label: string;
55
+ icon?: string;
56
+ path?: string;
57
+ children?: SidebarItem[];
58
+ }
59
+
60
+ /**
61
+ * 面包屑项
62
+ */
63
+ export interface BreadcrumbItem {
64
+ id: string;
65
+ name: string;
66
+ }
67
+
68
+ /**
69
+ * 操作结果
70
+ */
71
+ export interface OperationResult<T = void> {
72
+ success: boolean;
73
+ data?: T;
74
+ error?: string;
75
+ }
76
+
77
+ /** 压缩格式 */
78
+ export type CompressFormat = 'zip' | 'tar' | 'tgz' | 'tarbz2';
79
+
80
+ /** 压缩级别 */
81
+ export type CompressLevel = 'fast' | 'normal' | 'best';
82
+
83
+ /** 压缩选项 */
84
+ export interface CompressOptions {
85
+ format: CompressFormat;
86
+ level?: CompressLevel;
87
+ outputName: string;
88
+ outputDir: string;
89
+ deleteSource?: boolean;
90
+ }
91
+
92
+ /** 解压选项 */
93
+ export interface ExtractOptions {
94
+ targetDir: string;
95
+ deleteArchive?: boolean;
96
+ }
97
+
98
+ /** 压缩进度 */
99
+ export interface CompressProgress {
100
+ currentFile: string;
101
+ processedCount: number;
102
+ totalCount: number;
103
+ percent: number;
104
+ }
105
+
106
+ /** 压缩结果 */
107
+ export interface CompressResult {
108
+ success: boolean;
109
+ outputPath?: string;
110
+ error?: string;
111
+ }
112
+
113
+ /** 进度状态 */
114
+ export type ProgressStatus = 'pending' | 'processing' | 'success' | 'error';
115
+
116
+ /** 进度信息(UI 使用) */
117
+ export interface ProgressInfo {
118
+ type: 'compress' | 'extract';
119
+ status: ProgressStatus;
120
+ percent: number;
121
+ currentFile?: string;
122
+ processedCount?: number;
123
+ totalCount?: number;
124
+ error?: string;
125
+ outputPath?: string;
126
+ }
127
+
128
+ /**
129
+ * 文件操作适配器接口
130
+ */
131
+ export interface FileExplorerAdapter {
132
+ /** 读取目录 */
133
+ readDirectory(dirPath: string): Promise<FileItem[]>;
134
+ /** 读取系统路径目录 */
135
+ readSystemPath(pathId: string): Promise<FileItem[]>;
136
+ /** 获取系统路径 */
137
+ getSystemPath(pathId: string): Promise<string | null>;
138
+ /** 读取文件内容 */
139
+ readFileContent(filePath: string): Promise<string>;
140
+ /** 写入文件内容 */
141
+ writeFileContent(filePath: string, content: string): Promise<OperationResult>;
142
+ /** 创建文件夹 */
143
+ createFolder(parentDir: string, folderName: string): Promise<OperationResult<{ finalPath: string }>>;
144
+ /** 创建文件 */
145
+ createFile(parentDir: string, fileName: string, content?: string): Promise<OperationResult<{ finalPath: string }>>;
146
+ /** 删除文件 */
147
+ deleteFiles(paths: string[]): Promise<OperationResult>;
148
+ /** 重命名文件 */
149
+ renameFile(oldPath: string, newPath: string): Promise<OperationResult>;
150
+ /** 复制文件 */
151
+ copyFiles(sourcePaths: string[], targetDir: string): Promise<OperationResult<{ copiedPaths: string[] }>>;
152
+ /** 移动文件 */
153
+ moveFiles(sourcePaths: string[], targetDir: string): Promise<OperationResult<{ movedPaths: string[] }>>;
154
+ /** 使用系统默认应用打开 */
155
+ openPath(filePath: string): Promise<OperationResult>;
156
+ /** 复制文件到剪贴板 */
157
+ copyFilesToClipboard(filePaths: string[]): Promise<OperationResult>;
158
+ /** 获取剪贴板文件 */
159
+ getClipboardFiles(): Promise<OperationResult<{ files: string[] }>>;
160
+ /** 粘贴文件 */
161
+ pasteFiles(targetDir: string, sourcePaths: string[]): Promise<OperationResult<{ pastedPaths: string[] }>>;
162
+ /** 流式搜索文件 */
163
+ searchFilesStream(searchPath: string, pattern: string, searchId: string): Promise<OperationResult>;
164
+ /** 监听搜索结果 */
165
+ onSearchResults(callback: (data: { searchId: string; items: FileItem[]; done: boolean }) => void): () => void;
166
+ /** 压缩文件 */
167
+ compressFiles(sources: string[], options: CompressOptions): Promise<CompressResult>;
168
+ /** 解压文件 */
169
+ extractArchive(archivePath: string, options: ExtractOptions): Promise<CompressResult>;
170
+ /** 判断是否为压缩文件 */
171
+ isArchiveFile(filePath: string): Promise<boolean>;
172
+ /** 监听压缩进度 */
173
+ onCompressProgress(callback: (data: CompressProgress) => void): () => void;
174
+ /** 监听解压进度 */
175
+ onExtractProgress(callback: (data: CompressProgress) => void): () => void;
176
+ /** 监听目录变化 */
177
+ watchDirectory(dirPath: string, watchId: string): Promise<OperationResult>;
178
+ /** 停止监听目录 */
179
+ unwatchDirectory(watchId: string): Promise<OperationResult>;
180
+ /** 监听文件变化事件 */
181
+ onWatchEvent(callback: (data: { watchId: string; event: WatchEvent }) => void): () => void;
182
+ /** 显示文件/文件夹的系统属性窗口 */
183
+ showFileInfo(filePath: string): Promise<OperationResult>;
184
+ /** 在终端中打开目录 */
185
+ openInTerminal(dirPath: string): Promise<OperationResult>;
186
+ /** 通过 Cursor 打开 */
187
+ openInEditor(targetPath: string): Promise<OperationResult>;
188
+ /** 在新窗口中打开文件夹(使用系统文件管理器) */
189
+ openInNewWindow(folderPath: string): Promise<OperationResult>;
190
+ /** 请求窗口聚焦(用于右键打开菜单前激活窗口) */
191
+ requestWindowFocus(): void;
192
+
193
+ // ===== 媒体服务 =====
194
+
195
+ /** 检测媒体文件是否需要转码 */
196
+ mediaNeedsTranscode?(filePath: string): Promise<OperationResult<TranscodeInfo>>;
197
+ /** 获取可播放的媒体 URL(自动转码) */
198
+ mediaGetPlayableUrl?(filePath: string): Promise<{ success: boolean; url?: string; error?: string }>;
199
+ /** 获取媒体元数据 */
200
+ mediaGetMetadata?(filePath: string): Promise<OperationResult<MediaMetadata>>;
201
+ /** 监听转码进度 */
202
+ onMediaTranscodeProgress?(callback: (data: { filePath: string; progress: MediaTranscodeProgress }) => void): () => void;
203
+ /** 清理指定文件的转码缓存 */
204
+ mediaCleanupFile?(filePath: string): Promise<OperationResult>;
205
+
206
+ // ===== 媒体预览窗口 =====
207
+
208
+ /** 打开媒体预览窗口(原生窗口) */
209
+ openMediaPreviewWindow?(filePath: string, mediaType: 'image' | 'video' | 'audio'): Promise<OperationResult>;
210
+ /** 关闭媒体预览窗口 */
211
+ closeMediaPreviewWindow?(filePath: string): Promise<OperationResult>;
212
+ }
213
+
214
+ /** 文件变化事件类型 */
215
+ export type WatchEventType = 'add' | 'change' | 'remove' | 'rename';
216
+
217
+ /** 文件变化事件 */
218
+ export interface WatchEvent {
219
+ type: WatchEventType;
220
+ path: string;
221
+ filename: string;
222
+ }
223
+
224
+ // ===== 媒体服务类型 =====
225
+
226
+ /** 媒体转码方式 */
227
+ export type TranscodeMethod = 'direct' | 'remux' | 'transcode';
228
+
229
+ /** 媒体转码状态 */
230
+ export type TranscodeStatus = 'idle' | 'checking' | 'transcoding' | 'ready' | 'error';
231
+
232
+ /** 媒体转码信息 */
233
+ export interface TranscodeInfo {
234
+ type: 'video' | 'audio';
235
+ needsTranscode: boolean;
236
+ method: TranscodeMethod;
237
+ estimatedTime?: number;
238
+ targetFormat?: string;
239
+ }
240
+
241
+ /** 媒体转码进度 */
242
+ export interface MediaTranscodeProgress {
243
+ percent: number;
244
+ time?: number;
245
+ duration?: number;
246
+ speed?: string;
247
+ }
248
+
249
+ /** 媒体元数据 */
250
+ export interface MediaMetadata {
251
+ filePath: string;
252
+ type: 'video' | 'audio';
253
+ duration: number;
254
+ title?: string;
255
+ artist?: string;
256
+ album?: string;
257
+ }
258
+
259
+ /**
260
+ * 右键菜单项
261
+ */
262
+ export interface ContextMenuItem {
263
+ id: string;
264
+ label: string;
265
+ icon?: string;
266
+ shortcut?: string;
267
+ disabled?: boolean;
268
+ separator?: boolean;
269
+ danger?: boolean; // 危险操作标记(如删除)
270
+ checked?: boolean; // 勾选状态(显示在右侧)
271
+ action?: () => void;
272
+ children?: ContextMenuItem[];
273
+ }
274
+
@@ -0,0 +1,309 @@
1
+ import { FileType } from '../types';
2
+
3
+ /**
4
+ * 文件图标映射(基于 material-icon-theme.json)
5
+ *
6
+ * 原理:
7
+ * 1. KNOWN_TYPES:从 material-icon-theme.json 提取的所有图标名
8
+ * 2. EXT_MAP:扩展名 → 图标名
9
+ * 3. SPECIAL_FILES:特殊文件名 → 图标名
10
+ * 4. 逻辑:特殊文件名匹配 → 扩展名匹配 → 兜底
11
+ */
12
+
13
+ // 从 material-icon-theme.json 提取的所有图标名(~200 个常用)
14
+ const KNOWN_TYPES = new Set([
15
+ '3d', 'actionscript', 'ada', 'adobe-illustrator', 'adobe-photoshop', 'android',
16
+ 'angular', 'applescript', 'arduino', 'asciidoc', 'assembly', 'astro', 'audio',
17
+ 'aurelia', 'babel', 'ballerina', 'bazel', 'biome', 'blender', 'bower', 'bun',
18
+ 'c', 'cabal', 'caddy', 'cake', 'certificate', 'changelog', 'circleci', 'claude',
19
+ 'clojure', 'cmake', 'cobol', 'coffee', 'command', 'conduct', 'contributing',
20
+ 'controller', 'copilot', 'cpp', 'crystal', 'csharp', 'css', 'css-map', 'cucumber',
21
+ 'cuda', 'cursor', 'cypress', 'd', 'dart', 'database', 'deno', 'dependabot', 'diff',
22
+ 'django', 'docker', 'document', 'drawio', 'drizzle', 'editorconfig', 'ejs',
23
+ 'elixir', 'elm', 'email', 'ember', 'epub', 'erlang', 'esbuild', 'eslint',
24
+ 'excalidraw', 'exe', 'favicon', 'figma', 'firebase', 'flash', 'flow', 'font',
25
+ 'forth', 'fortran', 'freemarker', 'fsharp', 'gamemaker', 'gatsby', 'gcp', 'gemfile',
26
+ 'gemini', 'git', 'gitlab', 'gleam', 'go', 'go-mod', 'godot', 'gradle', 'graphql',
27
+ 'groovy', 'grunt', 'gulp', 'h', 'haml', 'handlebars', 'hardhat', 'haskell', 'haxe',
28
+ 'hcl', 'helm', 'hjson', 'hosts', 'hpp', 'html', 'http', 'husky', 'i18n', 'image',
29
+ 'imba', 'ionic', 'jar', 'java', 'javaclass', 'javascript', 'javascript-map',
30
+ 'jenkins', 'jest', 'jinja', 'jsconfig', 'json', 'julia', 'jupyter', 'just',
31
+ 'karma', 'key', 'kotlin', 'kubernetes', 'laravel', 'lean', 'lefthook', 'lerna',
32
+ 'less', 'lib', 'license', 'lighthouse', 'liquid', 'lisp', 'livescript', 'lock',
33
+ 'log', 'lua', 'luau', 'makefile', 'markdown', 'markdownlint', 'matlab', 'maven',
34
+ 'mdx', 'mercurial', 'mermaid', 'meson', 'minecraft', 'mjml', 'mocha', 'mojo',
35
+ 'nest', 'netlify', 'next', 'nginx', 'nim', 'nix', 'nodejs', 'nodemon', 'npm',
36
+ 'nuget', 'nunjucks', 'nuxt', 'nx', 'objective-c', 'objective-cpp', 'ocaml', 'odin',
37
+ 'openapi', 'opentofu', 'pascal', 'pdf', 'perl', 'php', 'phpstan', 'phpunit',
38
+ 'pipeline', 'playwright', 'pnpm', 'postcss', 'powerpoint', 'powershell', 'prettier',
39
+ 'prisma', 'processing', 'prolog', 'proto', 'pug', 'puppet', 'purescript', 'python',
40
+ 'pytorch', 'quasar', 'r', 'racket', 'razor', 'react', 'react-ts', 'readme', 'reason',
41
+ 'remark', 'remix', 'renovate', 'replit', 'rescript', 'riot', 'roadmap', 'robot',
42
+ 'robots', 'rollup', 'routing', 'rspec', 'rubocop', 'ruby', 'ruff', 'rust', 'salt',
43
+ 'san', 'sas', 'sass', 'sbt', 'scala', 'scheme', 'search', 'sentry', 'sequelize',
44
+ 'serverless', 'settings', 'shader', 'shellcheck', 'sketch', 'slim', 'slint',
45
+ 'smarty', 'snakemake', 'snapcraft', 'snyk', 'solidity', 'spwn', 'stackblitz',
46
+ 'stan', 'stencil', 'storybook', 'stryker', 'stylelint', 'stylus', 'sublime',
47
+ 'subtitles', 'supabase', 'svelte', 'svg', 'svgo', 'swagger', 'swc', 'swift',
48
+ 'tailwindcss', 'taskfile', 'tauri', 'tcl', 'teal', 'templ', 'template', 'terraform',
49
+ 'test-js', 'test-jsx', 'test-ts', 'tex', 'todo', 'toml', 'travis', 'tree', 'tsconfig',
50
+ 'tsdoc', 'turborepo', 'twig', 'typescript', 'typescript-def', 'unity', 'unocss',
51
+ 'vagrant', 'vanilla-extract', 'vercel', 'verilog', 'video', 'vim', 'vite', 'vitest',
52
+ 'vlang', 'vscode', 'vue', 'vue-config', 'wakatime', 'wallaby', 'webassembly',
53
+ 'webhint', 'webpack', 'windicss', 'word', 'wrangler', 'wxt', 'xaml', 'xmake',
54
+ 'xml', 'yaml', 'yarn', 'zig', 'zip',
55
+ ]);
56
+
57
+ // 扩展名 → 图标名
58
+ const EXT_MAP: Record<string, string> = {
59
+ // JavaScript/TypeScript
60
+ 'js': 'javascript', 'mjs': 'javascript', 'cjs': 'javascript',
61
+ 'jsx': 'react',
62
+ 'ts': 'typescript', 'mts': 'typescript', 'cts': 'typescript',
63
+ 'tsx': 'react-ts',
64
+ // 前端框架
65
+ 'vue': 'vue',
66
+ 'svelte': 'svelte',
67
+ 'astro': 'astro',
68
+ // 后端语言
69
+ 'py': 'python', 'pyw': 'python', 'pyi': 'python', 'pyc': 'python',
70
+ 'java': 'java', 'class': 'javaclass', 'jar': 'jar',
71
+ 'c': 'c', 'h': 'h',
72
+ 'cpp': 'cpp', 'cc': 'cpp', 'cxx': 'cpp', 'hpp': 'hpp', 'hh': 'hpp', 'hxx': 'hpp',
73
+ 'cs': 'csharp', 'csx': 'csharp',
74
+ 'go': 'go',
75
+ 'rs': 'rust',
76
+ 'php': 'php', 'phtml': 'php',
77
+ 'rb': 'ruby', 'rake': 'ruby',
78
+ 'swift': 'swift',
79
+ 'kt': 'kotlin', 'kts': 'kotlin',
80
+ 'scala': 'scala',
81
+ 'dart': 'dart',
82
+ 'lua': 'lua', 'luau': 'luau',
83
+ 'r': 'r', 'rdata': 'r', 'rds': 'r',
84
+ 'pl': 'perl', 'pm': 'perl',
85
+ 'sh': 'command', 'bash': 'command', 'zsh': 'command', 'fish': 'command',
86
+ 'ps1': 'powershell', 'psm1': 'powershell', 'psd1': 'powershell',
87
+ 'bat': 'command', 'cmd': 'command',
88
+ // 样式
89
+ 'css': 'css',
90
+ 'scss': 'sass', 'sass': 'sass',
91
+ 'less': 'less',
92
+ 'styl': 'stylus',
93
+ // 标记/配置
94
+ 'html': 'html', 'htm': 'html', 'xhtml': 'html',
95
+ 'xml': 'xml', 'xsl': 'xml', 'xslt': 'xml',
96
+ 'json': 'json', 'jsonc': 'json', 'json5': 'json',
97
+ 'yaml': 'yaml', 'yml': 'yaml',
98
+ 'toml': 'toml',
99
+ 'ini': 'settings',
100
+ 'conf': 'settings', 'config': 'settings',
101
+ // 文档
102
+ 'md': 'markdown', 'markdown': 'markdown', 'mdx': 'mdx',
103
+ 'txt': 'document',
104
+ 'pdf': 'pdf',
105
+ 'doc': 'word', 'docx': 'word', 'dot': 'word', 'dotx': 'word', 'odt': 'word',
106
+ 'xls': 'table', 'xlsx': 'table', 'xlsm': 'table', 'ods': 'table', 'csv': 'table',
107
+ 'ppt': 'powerpoint', 'pptx': 'powerpoint', 'odp': 'powerpoint',
108
+ // 图片
109
+ 'jpg': 'image', 'jpeg': 'image', 'png': 'image', 'gif': 'image', 'webp': 'image',
110
+ 'ico': 'image', 'bmp': 'image', 'tiff': 'image', 'tif': 'image', 'heic': 'image', 'heif': 'image',
111
+ 'svg': 'svg',
112
+ 'psd': 'adobe-photoshop',
113
+ 'ai': 'adobe-illustrator',
114
+ 'sketch': 'sketch',
115
+ 'fig': 'figma', 'figma': 'figma',
116
+ // 视频/音频
117
+ 'mp4': 'video', 'mov': 'video', 'avi': 'video', 'mkv': 'video', 'webm': 'video',
118
+ 'flv': 'video', 'wmv': 'video', 'm4v': 'video', '3gp': 'video', 'mpeg': 'video', 'mpg': 'video',
119
+ 'mp3': 'audio', 'wav': 'audio', 'flac': 'audio', 'aac': 'audio', 'ogg': 'audio',
120
+ 'wma': 'audio', 'm4a': 'audio', 'aiff': 'audio',
121
+ // 压缩
122
+ 'zip': 'zip', 'rar': 'zip', '7z': 'zip', 'tar': 'zip', 'gz': 'zip', 'bz2': 'zip',
123
+ 'xz': 'zip', 'tgz': 'zip', 'tbz2': 'zip',
124
+ // 数据库
125
+ 'sql': 'database',
126
+ 'db': 'database', 'sqlite': 'database', 'sqlite3': 'database',
127
+ 'prisma': 'prisma',
128
+ // 其他
129
+ 'log': 'log',
130
+ 'lock': 'lock',
131
+ 'env': 'settings',
132
+ 'graphql': 'graphql', 'gql': 'graphql',
133
+ 'proto': 'proto',
134
+ 'wasm': 'webassembly',
135
+ 'zig': 'zig',
136
+ 'nim': 'nim',
137
+ 'nix': 'nix',
138
+ 'hcl': 'hcl', 'tf': 'terraform',
139
+ 'sol': 'solidity',
140
+ 'ex': 'elixir', 'exs': 'elixir',
141
+ 'erl': 'erlang', 'hrl': 'erlang',
142
+ 'hs': 'haskell', 'lhs': 'haskell',
143
+ 'ml': 'ocaml', 'mli': 'ocaml',
144
+ 'clj': 'clojure', 'cljs': 'clojure', 'cljc': 'clojure',
145
+ 'lisp': 'lisp', 'lsp': 'lisp', 'el': 'lisp',
146
+ 'vim': 'vim',
147
+ 'dockerfile': 'docker',
148
+ };
149
+
150
+ // 特殊文件名 → 图标名
151
+ const SPECIAL_FILES: Record<string, string> = {
152
+ // Git
153
+ '.gitignore': 'git', '.gitattributes': 'git', '.gitmodules': 'git', '.gitkeep': 'git',
154
+ // 环境
155
+ '.env': 'settings', '.env.local': 'settings', '.env.development': 'settings',
156
+ '.env.production': 'settings', '.env.test': 'settings', '.env.example': 'settings',
157
+ // Node/包管理
158
+ 'package.json': 'nodejs', 'package-lock.json': 'npm',
159
+ 'yarn.lock': 'yarn', '.yarnrc': 'yarn', '.yarnrc.yml': 'yarn',
160
+ 'pnpm-lock.yaml': 'pnpm', '.pnpmfile.cjs': 'pnpm',
161
+ 'bun.lockb': 'bun', 'bunfig.toml': 'bun',
162
+ // Python
163
+ 'requirements.txt': 'python', 'pipfile': 'python', 'pipfile.lock': 'python',
164
+ 'pyproject.toml': 'python', 'poetry.lock': 'python', 'setup.py': 'python',
165
+ // Rust
166
+ 'cargo.toml': 'rust', 'cargo.lock': 'rust',
167
+ // Go
168
+ 'go.mod': 'go-mod', 'go.sum': 'go-mod', 'go.work': 'go-mod',
169
+ // PHP
170
+ 'composer.json': 'php', 'composer.lock': 'php',
171
+ // Ruby
172
+ 'gemfile': 'gemfile', 'gemfile.lock': 'gemfile', 'rakefile': 'ruby',
173
+ // Docker
174
+ 'dockerfile': 'docker', 'docker-compose.yml': 'docker', 'docker-compose.yaml': 'docker',
175
+ '.dockerignore': 'docker',
176
+ // 构建工具
177
+ 'makefile': 'makefile', 'gnumakefile': 'makefile',
178
+ 'cmakelists.txt': 'cmake',
179
+ 'build.gradle': 'gradle', 'build.gradle.kts': 'gradle', 'settings.gradle': 'gradle',
180
+ 'pom.xml': 'maven',
181
+ // 配置
182
+ 'tsconfig.json': 'tsconfig', 'jsconfig.json': 'jsconfig',
183
+ '.prettierrc': 'prettier', '.prettierrc.json': 'prettier', '.prettierrc.js': 'prettier',
184
+ '.prettierignore': 'prettier', 'prettier.config.js': 'prettier',
185
+ '.eslintrc': 'eslint', '.eslintrc.json': 'eslint', '.eslintrc.js': 'eslint',
186
+ '.eslintignore': 'eslint', 'eslint.config.js': 'eslint', 'eslint.config.mjs': 'eslint',
187
+ '.editorconfig': 'editorconfig',
188
+ 'vite.config.ts': 'vite', 'vite.config.js': 'vite',
189
+ 'webpack.config.js': 'webpack', 'webpack.config.ts': 'webpack',
190
+ 'rollup.config.js': 'rollup', 'rollup.config.ts': 'rollup',
191
+ 'esbuild.config.js': 'esbuild',
192
+ 'tailwind.config.js': 'tailwindcss', 'tailwind.config.ts': 'tailwindcss',
193
+ 'postcss.config.js': 'postcss', 'postcss.config.cjs': 'postcss',
194
+ 'babel.config.js': 'babel', '.babelrc': 'babel',
195
+ 'jest.config.js': 'jest', 'jest.config.ts': 'jest',
196
+ 'vitest.config.ts': 'vitest', 'vitest.config.js': 'vitest',
197
+ 'playwright.config.ts': 'playwright', 'playwright.config.js': 'playwright',
198
+ 'cypress.config.ts': 'cypress', 'cypress.config.js': 'cypress',
199
+ '.swcrc': 'swc', 'swc.config.js': 'swc',
200
+ 'turbo.json': 'turborepo',
201
+ 'nx.json': 'nx',
202
+ 'biome.json': 'biome',
203
+ '.nvmrc': 'nodejs', '.node-version': 'nodejs',
204
+ // 框架配置
205
+ 'nuxt.config.ts': 'nuxt', 'nuxt.config.js': 'nuxt',
206
+ 'next.config.js': 'next', 'next.config.mjs': 'next', 'next.config.ts': 'next',
207
+ 'svelte.config.js': 'svelte',
208
+ 'astro.config.mjs': 'astro', 'astro.config.ts': 'astro',
209
+ 'vue.config.js': 'vue-config',
210
+ 'angular.json': 'angular',
211
+ 'nest-cli.json': 'nest',
212
+ 'tauri.conf.json': 'tauri',
213
+ // CI/CD
214
+ '.travis.yml': 'travis',
215
+ '.gitlab-ci.yml': 'gitlab',
216
+ 'vercel.json': 'vercel',
217
+ 'netlify.toml': 'netlify',
218
+ // 其他
219
+ 'license': 'license', 'license.md': 'license', 'license.txt': 'license',
220
+ 'readme': 'readme', 'readme.md': 'readme', 'readme.txt': 'readme',
221
+ 'changelog': 'changelog', 'changelog.md': 'changelog',
222
+ '.npmrc': 'npm', '.npmignore': 'npm',
223
+ 'robots.txt': 'robots',
224
+ '.htaccess': 'nginx',
225
+ 'vagrantfile': 'vagrant',
226
+ '.stylelintrc': 'stylelint', '.stylelintrc.json': 'stylelint',
227
+ 'nodemon.json': 'nodemon',
228
+ '.huskyrc': 'husky',
229
+ 'renovate.json': 'renovate',
230
+ '.snyk': 'snyk',
231
+ 'deno.json': 'deno', 'deno.jsonc': 'deno',
232
+ };
233
+
234
+ /**
235
+ * 根据文件名获取 material-icon-theme 图标名称
236
+ */
237
+ export function getFileTypeIcon(fileName: string, fallbackType?: FileType): string {
238
+ const lowerName = fileName.toLowerCase();
239
+
240
+ // 1. 特殊文件名匹配
241
+ if (SPECIAL_FILES[lowerName]) {
242
+ const type = SPECIAL_FILES[lowerName];
243
+ if (KNOWN_TYPES.has(type)) {
244
+ return `material-icon-theme:${type}`;
245
+ }
246
+ }
247
+
248
+ // 2. Dockerfile 特殊处理
249
+ if (lowerName === 'dockerfile' || lowerName.startsWith('dockerfile.')) {
250
+ return 'material-icon-theme:docker';
251
+ }
252
+
253
+ // 3. .env 变体处理
254
+ if (lowerName === '.env' || lowerName.startsWith('.env.')) {
255
+ return 'material-icon-theme:settings';
256
+ }
257
+
258
+ // 4. 提取扩展名并匹配
259
+ const lastDotIndex = fileName.lastIndexOf('.');
260
+ const ext = lastDotIndex > 0 ? fileName.substring(lastDotIndex + 1).toLowerCase() : '';
261
+
262
+ // 处理复合扩展名
263
+ if (ext === 'ts' || ext === 'js') {
264
+ const baseName = fileName.substring(0, lastDotIndex).toLowerCase();
265
+ if (baseName.endsWith('.d')) {
266
+ return 'material-icon-theme:typescript-def';
267
+ }
268
+ if (baseName.endsWith('.test') || baseName.endsWith('.spec')) {
269
+ return ext === 'ts' ? 'material-icon-theme:test-ts' : 'material-icon-theme:test-js';
270
+ }
271
+ }
272
+ if (ext === 'jsx' || ext === 'tsx') {
273
+ const baseName = fileName.substring(0, lastDotIndex).toLowerCase();
274
+ if (baseName.endsWith('.test') || baseName.endsWith('.spec')) {
275
+ return ext === 'tsx' ? 'material-icon-theme:test-ts' : 'material-icon-theme:test-jsx';
276
+ }
277
+ }
278
+
279
+ if (ext && EXT_MAP[ext]) {
280
+ const type = EXT_MAP[ext];
281
+ if (KNOWN_TYPES.has(type)) {
282
+ return `material-icon-theme:${type}`;
283
+ }
284
+ }
285
+
286
+ // 5. 根据 fallbackType 返回兜底图标
287
+ if (fallbackType) {
288
+ return getFallbackIcon(fallbackType);
289
+ }
290
+
291
+ // 6. 最终兜底
292
+ return 'material-icon-theme:document';
293
+ }
294
+
295
+ function getFallbackIcon(type: FileType): string {
296
+ switch (type) {
297
+ case FileType.IMAGE: return 'material-icon-theme:image';
298
+ case FileType.VIDEO: return 'material-icon-theme:video';
299
+ case FileType.MUSIC: return 'material-icon-theme:audio';
300
+ case FileType.CODE: return 'material-icon-theme:javascript';
301
+ case FileType.TEXT: return 'material-icon-theme:document';
302
+ case FileType.DOCUMENT: return 'material-icon-theme:word';
303
+ case FileType.PDF: return 'material-icon-theme:pdf';
304
+ case FileType.ARCHIVE: return 'material-icon-theme:zip';
305
+ case FileType.APPLICATION: return 'material-icon-theme:exe';
306
+ default: return 'material-icon-theme:document';
307
+ }
308
+ }
309
+