@anymux/ui-kit 0.1.0
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.
- package/dist/ExplorerLayout-CSIJd7N4.js +105 -0
- package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
- package/dist/FileBrowserContext-B6jixa2j.js +11 -0
- package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
- package/dist/calendar-DSlrbHoj.js +761 -0
- package/dist/calendar-DSlrbHoj.js.map +1 -0
- package/dist/calendar.d.ts +3 -0
- package/dist/calendar.js +3 -0
- package/dist/contacts-DQXTZzHc.js +539 -0
- package/dist/contacts-DQXTZzHc.js.map +1 -0
- package/dist/contacts.d.ts +3 -0
- package/dist/contacts.js +3 -0
- package/dist/file-browser-m5atC3kF.js +6755 -0
- package/dist/file-browser-m5atC3kF.js.map +1 -0
- package/dist/file-browser.d.ts +11 -0
- package/dist/file-browser.js +9 -0
- package/dist/git-B55e6LL-.js +561 -0
- package/dist/git-B55e6LL-.js.map +1 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +3 -0
- package/dist/iconMap-V4B8P-Uh.js +206 -0
- package/dist/iconMap-V4B8P-Uh.js.map +1 -0
- package/dist/icons-CIsIOZXR.js +0 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +4 -0
- package/dist/index-BNmNIWBL.d.ts +71 -0
- package/dist/index-BNmNIWBL.d.ts.map +1 -0
- package/dist/index-Bryv_GCG.d.ts +1481 -0
- package/dist/index-Bryv_GCG.d.ts.map +1 -0
- package/dist/index-CuQIjSXs.d.ts +134 -0
- package/dist/index-CuQIjSXs.d.ts.map +1 -0
- package/dist/index-DSu19mq0.d.ts +153 -0
- package/dist/index-DSu19mq0.d.ts.map +1 -0
- package/dist/index-DmsyeHFr.d.ts +149 -0
- package/dist/index-DmsyeHFr.d.ts.map +1 -0
- package/dist/index-DxnJ8FYM.d.ts +17 -0
- package/dist/index-DxnJ8FYM.d.ts.map +1 -0
- package/dist/index-DzfY1Tok.d.ts +32 -0
- package/dist/index-DzfY1Tok.d.ts.map +1 -0
- package/dist/index-Ml_SgiKa.d.ts +1847 -0
- package/dist/index-Ml_SgiKa.d.ts.map +1 -0
- package/dist/index-kHr9udZD.d.ts +1025 -0
- package/dist/index-kHr9udZD.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/layout-Ca_4r8ka.js +89 -0
- package/dist/layout-Ca_4r8ka.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +5 -0
- package/dist/list-CxfT6hix.js +6831 -0
- package/dist/list-CxfT6hix.js.map +1 -0
- package/dist/list.d.ts +2 -0
- package/dist/list.js +5 -0
- package/dist/media-DZ292aKK.js +557 -0
- package/dist/media-DZ292aKK.js.map +1 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +3 -0
- package/dist/tree-Dd9Z0Aso.js +3351 -0
- package/dist/tree-Dd9Z0Aso.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +6 -0
- package/dist/types-common-CB3kRek8.d.ts +26 -0
- package/dist/types-common-CB3kRek8.d.ts.map +1 -0
- package/dist/utils-B4fdKKsy.js +3 -0
- package/package.json +109 -0
- package/src/calendar/AgendaView.tsx +37 -0
- package/src/calendar/CalendarBrowser.tsx +90 -0
- package/src/calendar/CalendarModel.ts +142 -0
- package/src/calendar/CalendarSidebar.tsx +81 -0
- package/src/calendar/DayView.tsx +76 -0
- package/src/calendar/EventCard.tsx +51 -0
- package/src/calendar/MockCalendarProvider.ts +98 -0
- package/src/calendar/MonthView.tsx +77 -0
- package/src/calendar/WeekView.tsx +129 -0
- package/src/calendar/index.ts +18 -0
- package/src/calendar/types.ts +25 -0
- package/src/contacts/ContactAvatar.tsx +35 -0
- package/src/contacts/ContactBrowser.tsx +56 -0
- package/src/contacts/ContactCard.tsx +37 -0
- package/src/contacts/ContactDetail.tsx +63 -0
- package/src/contacts/ContactGroupSidebar.tsx +40 -0
- package/src/contacts/ContactList.tsx +32 -0
- package/src/contacts/ContactListModel.ts +120 -0
- package/src/contacts/MockContactProvider.ts +77 -0
- package/src/contacts/index.ts +17 -0
- package/src/contacts/types.ts +26 -0
- package/src/demos/CalendarBrowserDemo.tsx +15 -0
- package/src/demos/ContactBrowserDemo.tsx +15 -0
- package/src/demos/MediaBrowserDemo.tsx +15 -0
- package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
- package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
- package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
- package/src/file-browser/adapters/README.md +504 -0
- package/src/file-browser/adapters/index.ts +27 -0
- package/src/file-browser/adapters/types.ts +70 -0
- package/src/file-browser/architecture.md +645 -0
- package/src/file-browser/components/CreateItemDialog.tsx +71 -0
- package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
- package/src/file-browser/components/FileBrowser.tsx +473 -0
- package/src/file-browser/components/FileBrowserContent.tsx +209 -0
- package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
- package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
- package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
- package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
- package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
- package/src/file-browser/components/PreviewPane.tsx +146 -0
- package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
- package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
- package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
- package/src/file-browser/components/UploadProgress.tsx +123 -0
- package/src/file-browser/components/ViewerHost.tsx +208 -0
- package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
- package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
- package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
- package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
- package/src/file-browser/components/shared/FileIcon.tsx +169 -0
- package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
- package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
- package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
- package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
- package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
- package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
- package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
- package/src/file-browser/context/FileBrowserContext.tsx +13 -0
- package/src/file-browser/examples/BasicUsage.tsx +20 -0
- package/src/file-browser/index.ts +98 -0
- package/src/file-browser/models/FileBrowserModel.ts +623 -0
- package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
- package/src/file-browser/models/NavigationManagerModel.ts +312 -0
- package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
- package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
- package/src/file-browser/models/SelectionManagerModel.ts +252 -0
- package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
- package/src/file-browser/models/UploadModel.ts +147 -0
- package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
- package/src/file-browser/models/ViewerHostModel.ts +44 -0
- package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
- package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
- package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
- package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
- package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
- package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
- package/src/file-browser/providers/FileSystemProvider.ts +401 -0
- package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
- package/src/file-browser/providers/GitProvider.ts +337 -0
- package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
- package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
- package/src/file-browser/providers/MemoryProvider.ts +303 -0
- package/src/file-browser/providers/index.ts +4 -0
- package/src/file-browser/registry/ViewerRegistry.ts +551 -0
- package/src/file-browser/registry/types.ts +144 -0
- package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
- package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
- package/src/file-browser/tasks.md +537 -0
- package/src/file-browser/types/FileBrowserTypes.ts +126 -0
- package/src/file-browser/types/ProviderTypes.ts +155 -0
- package/src/file-browser/types/UITypes.ts +235 -0
- package/src/file-browser/types/ViewModeTypes.ts +150 -0
- package/src/file-browser/utils/gestures.ts +327 -0
- package/src/file-browser/utils/performance.ts +563 -0
- package/src/file-browser/viewers/ImageViewer.tsx +163 -0
- package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
- package/src/file-browser/viewers/TextViewer.tsx +95 -0
- package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
- package/src/file-browser/viewers/index.ts +61 -0
- package/src/git/BranchList.tsx +128 -0
- package/src/git/CommitGraph.tsx +239 -0
- package/src/git/CommitList.tsx +258 -0
- package/src/git/DiffViewer.tsx +219 -0
- package/src/git/index.ts +4 -0
- package/src/icons/iconMap.ts +146 -0
- package/src/icons/index.ts +9 -0
- package/src/index.ts +13 -0
- package/src/layout/README.md +307 -0
- package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
- package/src/layout/examples/SimpleExample.tsx +60 -0
- package/src/layout/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/list/README.md +303 -0
- package/src/list/architecture.md +807 -0
- package/src/list/components/CalculatedGridView.tsx +252 -0
- package/src/list/components/DragPreview.tsx +102 -0
- package/src/list/components/ListContextMenu.tsx +274 -0
- package/src/list/components/ListItem.tsx +761 -0
- package/src/list/components/ListItems.tsx +919 -0
- package/src/list/components/MasonryView.tsx +241 -0
- package/src/list/components/SearchFilter.tsx +44 -0
- package/src/list/components/TreemapView.tsx +709 -0
- package/src/list/components/ViewSizeControls.tsx +205 -0
- package/src/list/components/ViewTypeSelector.tsx +312 -0
- package/src/list/components/VirtualizedDetailsView.tsx +231 -0
- package/src/list/components/VirtualizedGrid.tsx +164 -0
- package/src/list/components/VirtualizedList.tsx +154 -0
- package/src/list/components/VirtualizedMasonryView.tsx +344 -0
- package/src/list/components/shared/EmptyState.tsx +103 -0
- package/src/list/components/shared/ErrorBoundary.tsx +123 -0
- package/src/list/components/shared/ErrorDisplay.tsx +100 -0
- package/src/list/components/shared/ListLoader.tsx +146 -0
- package/src/list/components/shared/LoadingIndicator.tsx +80 -0
- package/src/list/index.ts +92 -0
- package/src/list/models/ListItemsModel.ts +1301 -0
- package/src/list/models/TreemapModel.ts +204 -0
- package/src/list/providers/ListItemsProvider.ts +313 -0
- package/src/list/providers/TestListProvider.ts +604 -0
- package/src/list/tasks.md +937 -0
- package/src/list/types/ListTypes.ts +178 -0
- package/src/list/utils/BenchmarkLogger.ts +243 -0
- package/src/list/utils/DragDropManager.ts +320 -0
- package/src/list/utils/GridLayoutCalculator.ts +290 -0
- package/src/list/utils/ListAccessibility.ts +367 -0
- package/src/list/utils/ListKeyboard.ts +414 -0
- package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
- package/src/list/utils/MasonryLayoutEngine.ts +401 -0
- package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
- package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
- package/src/media/AlbumSidebar.tsx +48 -0
- package/src/media/MediaBrowser.tsx +92 -0
- package/src/media/MediaBrowserModel.ts +138 -0
- package/src/media/MediaGrid.tsx +50 -0
- package/src/media/MediaList.tsx +49 -0
- package/src/media/MediaPreview.tsx +63 -0
- package/src/media/MediaTimeline.tsx +38 -0
- package/src/media/MockMediaProvider.ts +70 -0
- package/src/media/index.ts +18 -0
- package/src/media/types.ts +21 -0
- package/src/styles/variables.css +60 -0
- package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
- package/src/tree/__tests__/TreeModel.test.ts +16 -0
- package/src/tree/architecture.md +530 -0
- package/src/tree/components/Tree.tsx +283 -0
- package/src/tree/components/TreeCheckbox.tsx +147 -0
- package/src/tree/components/TreeContextMenu.tsx +139 -0
- package/src/tree/components/TreeNodeList.tsx +329 -0
- package/src/tree/components/TreeTable.tsx +382 -0
- package/src/tree/index.ts +58 -0
- package/src/tree/models/TreeModel.ts +839 -0
- package/src/tree/providers/SimpleTreeProvider.ts +463 -0
- package/src/tree/providers/TestTreeProvider.ts +946 -0
- package/src/tree/providers/TreeProvider.ts +308 -0
- package/src/tree/tasks.md +2046 -0
- package/src/tree/types/TreeTypes.ts +279 -0
- package/src/tree/utils/SelectionTheme.ts +150 -0
- package/src/tree/utils/logger.ts +203 -0
- package/src/types-common.ts +21 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { IFileSystem } from '@anymux/file-system';
|
|
2
|
+
import { IFileBrowserProvider } from './IFileBrowserProvider';
|
|
3
|
+
import { FileBrowserItem, IconDefinition, PreviewData } from '../types/FileBrowserTypes';
|
|
4
|
+
import { LeftPanelMode } from '../models/LeftPanelManagerModel';
|
|
5
|
+
import { ViewModeDefinition } from '../models/RightPanelManagerModel';
|
|
6
|
+
|
|
7
|
+
interface CacheEntry {
|
|
8
|
+
data: FileBrowserItem[];
|
|
9
|
+
timestamp: number;
|
|
10
|
+
lastAccessed: number;
|
|
11
|
+
accessCount: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CacheOptions {
|
|
15
|
+
maxSize: number;
|
|
16
|
+
ttlMs: number;
|
|
17
|
+
enableLogging: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class FileSystemProvider implements IFileBrowserProvider {
|
|
21
|
+
readonly id = 'filesystem';
|
|
22
|
+
readonly name = 'File System Provider';
|
|
23
|
+
readonly version = '1.0.0';
|
|
24
|
+
|
|
25
|
+
private fileSystem: IFileSystem;
|
|
26
|
+
private cache = new Map<string, CacheEntry>();
|
|
27
|
+
private cacheOptions: CacheOptions;
|
|
28
|
+
|
|
29
|
+
constructor(fileSystem: IFileSystem, cacheOptions?: Partial<CacheOptions>) {
|
|
30
|
+
this.fileSystem = fileSystem;
|
|
31
|
+
this.cacheOptions = {
|
|
32
|
+
maxSize: 100,
|
|
33
|
+
ttlMs: 30000, // 30 seconds
|
|
34
|
+
enableLogging: false,
|
|
35
|
+
...cacheOptions
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async loadItems(path: string): Promise<FileBrowserItem[]> {
|
|
40
|
+
try {
|
|
41
|
+
const normalizedPath = this.normalizePath(path);
|
|
42
|
+
|
|
43
|
+
// Check cache first
|
|
44
|
+
const cached = this.getFromCache(normalizedPath);
|
|
45
|
+
if (cached) {
|
|
46
|
+
this.logCache(`Cache hit for ${normalizedPath}`);
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.logCache(`Cache miss for ${normalizedPath}`);
|
|
51
|
+
const items = await this.fileSystem.readdir(normalizedPath, { withFileTypes: true });
|
|
52
|
+
|
|
53
|
+
// Load detailed information for each item
|
|
54
|
+
const itemsWithDetails = await Promise.all(
|
|
55
|
+
items.map(async (item) => {
|
|
56
|
+
const itemPath = this.resolvePath(normalizedPath, item.name);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Get detailed file information
|
|
60
|
+
const stats = await this.fileSystem.stat(itemPath);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
id: itemPath,
|
|
64
|
+
name: item.name,
|
|
65
|
+
path: itemPath,
|
|
66
|
+
type: item.isDirectory() ? 'directory' as const : 'file' as const,
|
|
67
|
+
hasChildren: item.isDirectory(),
|
|
68
|
+
size: stats.size,
|
|
69
|
+
lastModified: new Date(stats.mtime),
|
|
70
|
+
permissions: this.formatPermissions(stats.mode),
|
|
71
|
+
metadata: {
|
|
72
|
+
created: new Date(stats.birthtime || stats.ctime),
|
|
73
|
+
accessed: new Date(stats.atime),
|
|
74
|
+
isSymlink: stats.isSymbolicLink?.() || false,
|
|
75
|
+
blocks: stats.blocks,
|
|
76
|
+
blksize: stats.blksize,
|
|
77
|
+
dev: stats.dev,
|
|
78
|
+
ino: stats.ino,
|
|
79
|
+
nlink: stats.nlink,
|
|
80
|
+
rdev: stats.rdev,
|
|
81
|
+
uid: stats.uid,
|
|
82
|
+
gid: stats.gid,
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
} catch (statError) {
|
|
86
|
+
// If stat fails, return basic info without details
|
|
87
|
+
console.warn(`Failed to get stats for ${itemPath}:`, statError);
|
|
88
|
+
return {
|
|
89
|
+
id: itemPath,
|
|
90
|
+
name: item.name,
|
|
91
|
+
path: itemPath,
|
|
92
|
+
type: item.isDirectory() ? 'directory' as const : 'file' as const,
|
|
93
|
+
hasChildren: item.isDirectory(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Store in cache
|
|
100
|
+
this.setInCache(normalizedPath, itemsWithDetails);
|
|
101
|
+
|
|
102
|
+
return itemsWithDetails;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Invalidate cache on error
|
|
105
|
+
this.invalidateCache(this.normalizePath(path));
|
|
106
|
+
|
|
107
|
+
const enhancedError = this.enhanceError(error, path, 'loadItems');
|
|
108
|
+
console.error('Failed to load items:', enhancedError);
|
|
109
|
+
throw enhancedError;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
normalizePath(path: string): string {
|
|
114
|
+
// Normalize path separators and remove duplicate slashes
|
|
115
|
+
if (!path || path === '') return '/';
|
|
116
|
+
|
|
117
|
+
// Convert backslashes to forward slashes
|
|
118
|
+
let normalized = path.replace(/\\/g, '/');
|
|
119
|
+
|
|
120
|
+
// Remove duplicate slashes
|
|
121
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
122
|
+
|
|
123
|
+
// Remove trailing slash unless it's root
|
|
124
|
+
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
125
|
+
normalized = normalized.slice(0, -1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Ensure it starts with /
|
|
129
|
+
if (!normalized.startsWith('/')) {
|
|
130
|
+
normalized = '/' + normalized;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private formatPermissions(mode: number | undefined): string {
|
|
137
|
+
if (mode === undefined) return '';
|
|
138
|
+
|
|
139
|
+
// Extract permission bits (last 9 bits)
|
|
140
|
+
const perms = mode & parseInt('777', 8);
|
|
141
|
+
|
|
142
|
+
// Convert to rwx format
|
|
143
|
+
const octal = perms.toString(8).padStart(3, '0');
|
|
144
|
+
let result = '';
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < 3; i++) {
|
|
147
|
+
const digit = parseInt(octal[i] || '0', 10);
|
|
148
|
+
result += (digit & 4) ? 'r' : '-';
|
|
149
|
+
result += (digit & 2) ? 'w' : '-';
|
|
150
|
+
result += (digit & 1) ? 'x' : '-';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private enhanceError(error: any, path: string, operation: string): Error {
|
|
157
|
+
const message = error?.message || 'Unknown error';
|
|
158
|
+
const code = error?.code || 'UNKNOWN';
|
|
159
|
+
|
|
160
|
+
const enhancedError = new Error(
|
|
161
|
+
`${operation} failed for path "${path}": ${message}`
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Preserve error properties
|
|
165
|
+
(enhancedError as any).code = code;
|
|
166
|
+
(enhancedError as any).path = path;
|
|
167
|
+
(enhancedError as any).operation = operation;
|
|
168
|
+
(enhancedError as any).originalError = error;
|
|
169
|
+
(enhancedError as any).timestamp = new Date();
|
|
170
|
+
|
|
171
|
+
return enhancedError;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getParentPath(path: string): string | null {
|
|
175
|
+
const normalized = this.normalizePath(path);
|
|
176
|
+
if (normalized === '/') return null;
|
|
177
|
+
|
|
178
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
179
|
+
parts.pop();
|
|
180
|
+
|
|
181
|
+
return parts.length === 0 ? '/' : '/' + parts.join('/');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
resolvePath(basePath: string, relativePath: string): string {
|
|
185
|
+
if (relativePath.startsWith('/')) {
|
|
186
|
+
return this.normalizePath(relativePath);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const base = this.normalizePath(basePath);
|
|
190
|
+
const resolved = base === '/' ? `/${relativePath}` : `${base}/${relativePath}`;
|
|
191
|
+
return this.normalizePath(resolved);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getItemIcon(item: FileBrowserItem): IconDefinition {
|
|
195
|
+
if (item.type === 'directory') {
|
|
196
|
+
return {
|
|
197
|
+
type: 'lucide',
|
|
198
|
+
name: 'folder',
|
|
199
|
+
color: '#3b82f6'
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Simple file type detection based on extension
|
|
204
|
+
const ext = item.name.split('.').pop()?.toLowerCase();
|
|
205
|
+
const iconMap: Record<string, string> = {
|
|
206
|
+
'js': 'file-code',
|
|
207
|
+
'ts': 'file-code',
|
|
208
|
+
'jsx': 'file-code',
|
|
209
|
+
'tsx': 'file-code',
|
|
210
|
+
'html': 'file-code',
|
|
211
|
+
'css': 'file-code',
|
|
212
|
+
'json': 'file-code',
|
|
213
|
+
'md': 'file-text',
|
|
214
|
+
'txt': 'file-text',
|
|
215
|
+
'png': 'image',
|
|
216
|
+
'jpg': 'image',
|
|
217
|
+
'jpeg': 'image',
|
|
218
|
+
'gif': 'image',
|
|
219
|
+
'svg': 'image',
|
|
220
|
+
'pdf': 'file-text',
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
type: 'lucide',
|
|
225
|
+
name: iconMap[ext || ''] || 'file',
|
|
226
|
+
color: '#6b7280'
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
getItemDisplayName(item: FileBrowserItem): string {
|
|
231
|
+
return item.name;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async getItemPreview(item: FileBrowserItem): Promise<PreviewData | null> {
|
|
235
|
+
// Basic preview support - this could be extended
|
|
236
|
+
if (item.type === 'file') {
|
|
237
|
+
const ext = item.name.split('.').pop()?.toLowerCase();
|
|
238
|
+
|
|
239
|
+
if (['png', 'jpg', 'jpeg', 'gif', 'svg'].includes(ext || '')) {
|
|
240
|
+
return {
|
|
241
|
+
type: 'image',
|
|
242
|
+
url: item.path // This would need to be a proper URL in a real implementation
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (['txt', 'md', 'json', 'js', 'ts', 'css', 'html'].includes(ext || '')) {
|
|
247
|
+
try {
|
|
248
|
+
// For text files, we could read and preview content
|
|
249
|
+
return {
|
|
250
|
+
type: 'text',
|
|
251
|
+
content: 'Preview not implemented'
|
|
252
|
+
};
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
canPerformAction(action: string, items: FileBrowserItem[]): boolean {
|
|
263
|
+
// Basic action support
|
|
264
|
+
const supportedActions = ['delete', 'rename', 'copy', 'move'];
|
|
265
|
+
return supportedActions.includes(action) && items.length > 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async performAction(action: string, items: FileBrowserItem[]): Promise<any> {
|
|
269
|
+
// This would implement actual file operations
|
|
270
|
+
// For now, just return success
|
|
271
|
+
console.log(`Performing action ${action} on items:`, items);
|
|
272
|
+
return { success: true, message: `${action} completed` };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async initialize(): Promise<void> {
|
|
276
|
+
// Any initialization needed
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
dispose(): void {
|
|
280
|
+
// Clear cache
|
|
281
|
+
this.cache.clear();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Cache management methods
|
|
285
|
+
private getFromCache(path: string): FileBrowserItem[] | null {
|
|
286
|
+
const entry = this.cache.get(path);
|
|
287
|
+
if (!entry) return null;
|
|
288
|
+
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
|
|
291
|
+
// Check if expired
|
|
292
|
+
if (now - entry.timestamp > this.cacheOptions.ttlMs) {
|
|
293
|
+
this.cache.delete(path);
|
|
294
|
+
this.logCache(`Cache entry expired for ${path}`);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Update access stats
|
|
299
|
+
entry.lastAccessed = now;
|
|
300
|
+
entry.accessCount++;
|
|
301
|
+
|
|
302
|
+
return entry.data;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private setInCache(path: string, data: FileBrowserItem[]): void {
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
|
|
308
|
+
// Evict oldest entries if cache is full
|
|
309
|
+
if (this.cache.size >= this.cacheOptions.maxSize) {
|
|
310
|
+
this.evictLeastRecentlyUsed();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.cache.set(path, {
|
|
314
|
+
data: [...data], // Clone to prevent mutations
|
|
315
|
+
timestamp: now,
|
|
316
|
+
lastAccessed: now,
|
|
317
|
+
accessCount: 1
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
this.logCache(`Cached ${data.length} items for ${path}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private invalidateCache(path: string): void {
|
|
324
|
+
if (this.cache.has(path)) {
|
|
325
|
+
this.cache.delete(path);
|
|
326
|
+
this.logCache(`Invalidated cache for ${path}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private evictLeastRecentlyUsed(): void {
|
|
331
|
+
let oldestPath: string | null = null;
|
|
332
|
+
let oldestAccess = Date.now();
|
|
333
|
+
|
|
334
|
+
for (const [path, entry] of this.cache.entries()) {
|
|
335
|
+
if (entry.lastAccessed < oldestAccess) {
|
|
336
|
+
oldestAccess = entry.lastAccessed;
|
|
337
|
+
oldestPath = path;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (oldestPath) {
|
|
342
|
+
this.cache.delete(oldestPath);
|
|
343
|
+
this.logCache(`Evicted LRU cache entry: ${oldestPath}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private logCache(message: string): void {
|
|
348
|
+
if (this.cacheOptions.enableLogging) {
|
|
349
|
+
console.log(`[FileSystemProvider Cache] ${message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Public cache methods for external management
|
|
354
|
+
clearCache(): void {
|
|
355
|
+
this.cache.clear();
|
|
356
|
+
this.logCache('Cache cleared');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
getCacheStats(): { size: number; maxSize: number; hitRate?: number } {
|
|
360
|
+
return {
|
|
361
|
+
size: this.cache.size,
|
|
362
|
+
maxSize: this.cacheOptions.maxSize
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Dual-panel support
|
|
367
|
+
getLeftPanelModes(): LeftPanelMode[] {
|
|
368
|
+
return [
|
|
369
|
+
{
|
|
370
|
+
id: 'tree',
|
|
371
|
+
name: 'Tree',
|
|
372
|
+
icon: 'folder-tree',
|
|
373
|
+
component: {} as any, // Will be set by consumer
|
|
374
|
+
isAvailable: () => true
|
|
375
|
+
}
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
getRightPanelViewModes(): ViewModeDefinition[] {
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
id: 'list',
|
|
383
|
+
name: 'List',
|
|
384
|
+
icon: 'list',
|
|
385
|
+
applicableFor: (contentType) => contentType === 'folder'
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
id: 'thumbnail',
|
|
389
|
+
name: 'Thumbnails',
|
|
390
|
+
icon: 'grid-3x3',
|
|
391
|
+
applicableFor: (contentType) => contentType === 'folder'
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
id: 'detail',
|
|
395
|
+
name: 'Details',
|
|
396
|
+
icon: 'info',
|
|
397
|
+
applicableFor: (contentType) => contentType === 'folder'
|
|
398
|
+
}
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { makeAutoObservable } from 'mobx';
|
|
2
|
+
import type { TreeProvider, TreeSelectionInfo } from '../../tree/providers/TreeProvider';
|
|
3
|
+
import type { TreeNodeData, TreeLoadOptions, TreeLoadResult, TreeContextMenuItem } from '../../tree/types/TreeTypes';
|
|
4
|
+
import { FileSystemBridge, type FileSystemItem } from '../adapters/FileSystemBridge';
|
|
5
|
+
|
|
6
|
+
interface FileSystemTreeProviderOptions {
|
|
7
|
+
showFilesInTree: boolean;
|
|
8
|
+
onSelectionChange: (path: string) => void;
|
|
9
|
+
onRefresh?: () => Promise<void>;
|
|
10
|
+
onRenameRequest?: (itemId: string, currentName: string, path: string, source: 'list' | 'tree') => void;
|
|
11
|
+
onDeleteRequest?: (targets: Array<{ path: string; isDirectory: boolean; name: string }>) => void;
|
|
12
|
+
onNewItemRequest?: (parentPath: string, type: 'file' | 'folder') => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class FileSystemTreeProvider implements TreeProvider {
|
|
16
|
+
readonly id = 'filesystem-tree';
|
|
17
|
+
readonly name = 'File System Tree';
|
|
18
|
+
readonly version = '1.0.0';
|
|
19
|
+
|
|
20
|
+
isMultiSelectEnabled = false;
|
|
21
|
+
readonly isDragDropEnabled = true;
|
|
22
|
+
readonly isVirtualizationEnabled = false;
|
|
23
|
+
readonly useCheckboxSelection = false;
|
|
24
|
+
readonly allowPartialSelection = false;
|
|
25
|
+
readonly isTableViewEnabled = false;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private bridge: FileSystemBridge,
|
|
29
|
+
private options: FileSystemTreeProviderOptions
|
|
30
|
+
) {
|
|
31
|
+
makeAutoObservable(this, {});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async loadNodes(options?: TreeLoadOptions): Promise<TreeLoadResult> {
|
|
35
|
+
const rootItems = await this.bridge.loadDirectoryTree('/');
|
|
36
|
+
|
|
37
|
+
const nodes: TreeNodeData[] = rootItems.map(item => ({
|
|
38
|
+
id: item.id,
|
|
39
|
+
name: item.name,
|
|
40
|
+
path: item.path,
|
|
41
|
+
type: 'directory',
|
|
42
|
+
hasChildren: true,
|
|
43
|
+
isExpanded: false,
|
|
44
|
+
metadata: item
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
nodes,
|
|
49
|
+
totalCount: nodes.length
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async loadChildren(node: TreeNodeData, options?: TreeLoadOptions): Promise<TreeLoadResult> {
|
|
54
|
+
try {
|
|
55
|
+
const items = await this.bridge.loadDirectoryTree(node.path);
|
|
56
|
+
|
|
57
|
+
const childNodes: TreeNodeData[] = items.map(item => ({
|
|
58
|
+
id: item.id,
|
|
59
|
+
name: item.name,
|
|
60
|
+
path: item.path,
|
|
61
|
+
type: 'directory',
|
|
62
|
+
hasChildren: true,
|
|
63
|
+
isExpanded: false,
|
|
64
|
+
metadata: item
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
nodes: childNodes,
|
|
69
|
+
totalCount: childNodes.length
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return { nodes: [], totalCount: 0 };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async refresh(path?: string): Promise<TreeLoadResult> {
|
|
77
|
+
return this.loadNodes();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getNodeContextMenu(node: TreeNodeData): TreeContextMenuItem[] {
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
id: 'open',
|
|
84
|
+
label: 'Open',
|
|
85
|
+
icon: 'folder-open',
|
|
86
|
+
type: 'item',
|
|
87
|
+
handler: () => this.options.onSelectionChange(node.path)
|
|
88
|
+
},
|
|
89
|
+
{ id: 'separator1', type: 'separator', label: '' },
|
|
90
|
+
{
|
|
91
|
+
id: 'new-folder',
|
|
92
|
+
label: 'New Folder',
|
|
93
|
+
icon: 'folder-plus',
|
|
94
|
+
type: 'item',
|
|
95
|
+
handler: () => this.requestNewItem(node.path, 'folder')
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'new-file',
|
|
99
|
+
label: 'New File',
|
|
100
|
+
icon: 'file-plus',
|
|
101
|
+
type: 'item',
|
|
102
|
+
handler: () => this.requestNewItem(node.path, 'file')
|
|
103
|
+
},
|
|
104
|
+
{ id: 'separator2', type: 'separator', label: '' },
|
|
105
|
+
{
|
|
106
|
+
id: 'rename',
|
|
107
|
+
label: 'Rename',
|
|
108
|
+
icon: 'edit',
|
|
109
|
+
type: 'item',
|
|
110
|
+
handler: () => this.requestRename(node)
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'delete',
|
|
114
|
+
label: 'Delete',
|
|
115
|
+
icon: 'trash',
|
|
116
|
+
type: 'item',
|
|
117
|
+
handler: () => this.requestDelete([node])
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getMultiNodeContextMenu(nodes: TreeNodeData[]): TreeContextMenuItem[] {
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
id: 'delete-multiple',
|
|
126
|
+
label: `Delete ${nodes.length} Folders`,
|
|
127
|
+
icon: 'trash',
|
|
128
|
+
type: 'item',
|
|
129
|
+
handler: () => this.requestDelete(nodes)
|
|
130
|
+
}
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getEmptySpaceContextMenu(parentNode?: TreeNodeData): TreeContextMenuItem[] {
|
|
135
|
+
const parentPath = parentNode?.path || '/';
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
id: 'new-folder',
|
|
139
|
+
label: 'New Folder',
|
|
140
|
+
icon: 'folder-plus',
|
|
141
|
+
type: 'item',
|
|
142
|
+
handler: () => this.requestNewItem(parentPath, 'folder')
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'new-file',
|
|
146
|
+
label: 'New File',
|
|
147
|
+
icon: 'file-plus',
|
|
148
|
+
type: 'item',
|
|
149
|
+
handler: () => this.requestNewItem(parentPath, 'file')
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
canExpand(node: TreeNodeData): boolean {
|
|
155
|
+
return node.type === 'directory';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
canSelect(node: TreeNodeData): boolean {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
onSelectionChange = (selectionInfo: TreeSelectionInfo) => {
|
|
163
|
+
if (selectionInfo.selectedNodes.length > 0) {
|
|
164
|
+
const selectedNode = selectionInfo.selectedNodes[0];
|
|
165
|
+
if (selectedNode) {
|
|
166
|
+
this.options.onSelectionChange(selectedNode.path);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
onNodeExpansion?(node: TreeNodeData, expanded: boolean): void {}
|
|
172
|
+
|
|
173
|
+
onNodeDoubleClick?(node: TreeNodeData): void {
|
|
174
|
+
this.options.onSelectionChange(node.path);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
onNodeFocus?(node: TreeNodeData | null): void {}
|
|
178
|
+
|
|
179
|
+
onContextMenuAction?(menuItemId: string, nodes: TreeNodeData[]): void {}
|
|
180
|
+
|
|
181
|
+
private requestRename(node: TreeNodeData) {
|
|
182
|
+
if (this.options.onRenameRequest) {
|
|
183
|
+
const currentName = node.path.split('/').pop() || '';
|
|
184
|
+
this.options.onRenameRequest(node.id, currentName, node.path, 'tree');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private requestDelete(nodes: TreeNodeData[]) {
|
|
189
|
+
if (this.options.onDeleteRequest) {
|
|
190
|
+
const targets = nodes.map(n => ({
|
|
191
|
+
path: n.path,
|
|
192
|
+
isDirectory: true,
|
|
193
|
+
name: n.path.split('/').pop() || ''
|
|
194
|
+
}));
|
|
195
|
+
this.options.onDeleteRequest(targets);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private requestNewItem(parentPath: string, type: 'file' | 'folder') {
|
|
200
|
+
if (this.options.onNewItemRequest) {
|
|
201
|
+
this.options.onNewItemRequest(parentPath, type);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async executeRename(path: string, newName: string): Promise<void> {
|
|
206
|
+
const parentPath = path.split('/').slice(0, -1).join('/') || '/';
|
|
207
|
+
const newPath = `${parentPath}/${newName}`.replace(/\/\//g, '/');
|
|
208
|
+
await this.bridge.renameItem(path, newPath);
|
|
209
|
+
await this.options.onRefresh?.();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async executeDelete(targets: Array<{ path: string; isDirectory: boolean }>): Promise<void> {
|
|
213
|
+
await Promise.all(targets.map(t =>
|
|
214
|
+
this.bridge.deleteItem(t.path, t.isDirectory)
|
|
215
|
+
));
|
|
216
|
+
await this.options.onRefresh?.();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async executeCreate(parentPath: string, name: string, type: 'file' | 'folder'): Promise<void> {
|
|
220
|
+
const newPath = `${parentPath}/${name}`.replace(/\/\//g, '/');
|
|
221
|
+
if (type === 'folder') {
|
|
222
|
+
await this.bridge.createDirectory(newPath);
|
|
223
|
+
} else {
|
|
224
|
+
await this.bridge.writeFile(newPath, '');
|
|
225
|
+
}
|
|
226
|
+
await this.options.onRefresh?.();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async initialize?(): Promise<void> {}
|
|
230
|
+
dispose?(): void {}
|
|
231
|
+
}
|